From 0df1bc9b6038f36733ada16d024a43191f974a73 Mon Sep 17 00:00:00 2001 From: Filippos Sakellaropoulos Date: Fri, 21 Jun 2024 01:59:10 +0300 Subject: [PATCH 01/12] save DeferredIssueModel data if deferred issuance occurs --- Sources/EudiWalletKit/EudiWallet.swift | 15 +++++++-------- .../Services/OpenId4VciService.swift | 8 +++++--- .../EudiWalletKit/Services/StorageManager.swift | 17 +++++++++++++++++ .../ViewModels/OfferedDocModel.swift | 1 + 4 files changed, 30 insertions(+), 11 deletions(-) diff --git a/Sources/EudiWalletKit/EudiWallet.swift b/Sources/EudiWalletKit/EudiWallet.swift index 2a395c8..e89c13c 100644 --- a/Sources/EudiWalletKit/EudiWallet.swift +++ b/Sources/EudiWalletKit/EudiWallet.swift @@ -99,16 +99,15 @@ public final class EudiWallet: ObservableObject { } func finalizeIssuing(id: String, data: Data, docType: String?, format: DataFormat, issueReq: IssueRequest, openId4VCIService: OpenId4VCIService) async throws -> WalletStorage.Document { - let iss = IssuerSigned(data: [UInt8](data)) - let deviceResponse = iss != nil ? nil : DeviceResponse(data: [UInt8](data)) - guard let ddt = DocDataType(rawValue: format.rawValue) else { throw WalletError(description: "Invalid format \(format.rawValue)") } - let docTypeToSave = docType ?? (format == .cbor ? iss?.issuerAuth.mso.docType ?? deviceResponse?.documents?.first?.docType : nil) - var dataToSave: Data? = data - if let deviceResponse { - if let iss = deviceResponse.documents?.first?.issuerSigned { dataToSave = Data(iss.encode(options: CBOROptions())) } else { dataToSave = nil } + var dataToSave: Data = data + guard var ddt = DocDataType(rawValue: format.rawValue) else { throw WalletError(description: "Invalid format \(format.rawValue)") } + if var defTO = try? CodableCBORDecoder().decode(DeferredIssueModel.self, from: data) { + ddt = DocDataType.deferred + defTO.docType = docType; defTO.format = format.rawValue + dataToSave = try! CodableCBOREncoder().encode(defTO) } + let docTypeToSave = if format == .cbor { IssuerSigned(data: [UInt8](data))?.issuerAuth.mso.docType ?? docType } else { docType } guard let docTypeToSave else { throw WalletError(description: "Unknown document type") } - guard let dataToSave else { throw WalletError(description: "Issued data cannot be recognized") } var issued: WalletStorage.Document if !openId4VCIService.usedSecureEnclave { issued = WalletStorage.Document(id: id, docType: docTypeToSave, docDataType: ddt, data: dataToSave, privateKeyType: .x963EncodedP256, privateKey: issueReq.keyData, createdAt: Date()) diff --git a/Sources/EudiWalletKit/Services/OpenId4VciService.swift b/Sources/EudiWalletKit/Services/OpenId4VciService.swift index 7b34964..e3bab0d 100644 --- a/Sources/EudiWalletKit/Services/OpenId4VciService.swift +++ b/Sources/EudiWalletKit/Services/OpenId4VciService.swift @@ -23,6 +23,7 @@ import Logging import CryptoKit import Security import WalletStorage +import SwiftCBOR public class OpenId4VCIService: NSObject, ASWebAuthenticationPresentationContextProviding { let issueReq: IssueRequest @@ -98,7 +99,7 @@ public class OpenId4VCIService: NSObject, ASWebAuthenticationPresentationContext let data = try await credentialInfo.asyncCompactMap { do { logger.info("Starting issuing with identifer \($0.identifier.value) and scope \($0.scope)") - let str = try await issueOfferedCredentialWithProof(authorized, offer: offer, issuer: issuer, credentialConfigurationIdentifier: $0.identifier, claimSet: claimSet) + let str = try await issueOfferedCredentialInternalValidated(authorized, offer: offer, issuer: issuer, credentialConfigurationIdentifier: $0.identifier, claimSet: claimSet) return Data(base64URLEncoded: str) } catch { logger.error("Failed to issue document with scope \($0.scope)") @@ -140,7 +141,7 @@ public class OpenId4VCIService: NSObject, ASWebAuthenticationPresentationContext } } - private func issueOfferedCredentialWithProof(_ authorized: AuthorizedRequest, offer: CredentialOffer, issuer: Issuer, credentialConfigurationIdentifier: CredentialConfigurationIdentifier, claimSet: ClaimSet? = nil) async throws -> String { + private func issueOfferedCredentialInternalValidated(_ authorized: AuthorizedRequest, offer: CredentialOffer, issuer: Issuer, credentialConfigurationIdentifier: CredentialConfigurationIdentifier, claimSet: ClaimSet? = nil) async throws -> String { let issuerMetadata = offer.credentialIssuerMetadata guard issuerMetadata.credentialsSupported.keys.contains(where: { $0.value == credentialConfigurationIdentifier.value }) else { throw WalletError(description: "Cannot find credential identifier \(credentialConfigurationIdentifier.value) in offer") @@ -289,7 +290,8 @@ public class OpenId4VCIService: NSObject, ASWebAuthenticationPresentationContext case .issued(_, let credential): return credential case .issuancePending(let transactionId): - throw WalletError(description: "Credential not ready yet. Try after \(transactionId.interval ?? 0)") + logger.info("Credential not ready yet. Try after \(transactionId.interval ?? 0)") + return (try! CodableCBOREncoder().encode(DeferredIssueModel(credentialIssuerUrl: credentialIssuerURL, accessToken: authorized.accessToken?.accessToken ?? "", refreshToken: nil, transactionId: transactionId.value))).base64URLEncodedString() case .errored(_, let errorDescription): throw WalletError(description: "\(errorDescription ?? "Something went wrong with your deferred request response")") } diff --git a/Sources/EudiWalletKit/Services/StorageManager.swift b/Sources/EudiWalletKit/Services/StorageManager.swift index ba80a07..a4a6ba0 100644 --- a/Sources/EudiWalletKit/Services/StorageManager.swift +++ b/Sources/EudiWalletKit/Services/StorageManager.swift @@ -186,5 +186,22 @@ public class StorageManager: ObservableObject { } +public struct DeferredIssueModel: Codable { + let credentialIssuerUrl: String + var docType: String? + var format: String = DataFormat.cbor.rawValue + let accessToken: String + let refreshToken: String? + let transactionId: String + + public init(credentialIssuerUrl: String, docType: String? = nil, accessToken: String, refreshToken: String?, transactionId: String) { + self.credentialIssuerUrl = credentialIssuerUrl + self.docType = docType + self.accessToken = accessToken + self.refreshToken = refreshToken + self.transactionId = transactionId + } +} + diff --git a/Sources/EudiWalletKit/ViewModels/OfferedDocModel.swift b/Sources/EudiWalletKit/ViewModels/OfferedDocModel.swift index a014cce..f51b8d8 100644 --- a/Sources/EudiWalletKit/ViewModels/OfferedDocModel.swift +++ b/Sources/EudiWalletKit/ViewModels/OfferedDocModel.swift @@ -21,3 +21,4 @@ public struct OfferedDocModel { public let docType: String public let displayName: String } + From afe1806799940dcaf398eb74faf0045428a915ec Mon Sep 17 00:00:00 2001 From: Filippos Sakellaropoulos Date: Thu, 11 Jul 2024 02:49:53 +0300 Subject: [PATCH 02/12] modifications to support deferred document to be saved --- Sources/EudiWalletKit/EudiWallet.swift | 22 ++++--- .../Services/OpenId4VciService.swift | 57 ++++++++++++------- .../Services/StorageManager.swift | 16 ------ .../ViewModels/DeferredIssueModel.swift | 33 +++++++++++ 4 files changed, 82 insertions(+), 46 deletions(-) create mode 100644 Sources/EudiWalletKit/ViewModels/DeferredIssueModel.swift diff --git a/Sources/EudiWalletKit/EudiWallet.swift b/Sources/EudiWalletKit/EudiWallet.swift index b029782..a8a8cdf 100644 --- a/Sources/EudiWalletKit/EudiWallet.swift +++ b/Sources/EudiWalletKit/EudiWallet.swift @@ -101,16 +101,20 @@ public final class EudiWallet: ObservableObject { return try await finalizeIssuing(id: id, data: data, docType: docType, format: format, issueReq: issueReq, openId4VCIService: openId4VCIService) } - func finalizeIssuing(id: String, data: Data, docType: String?, format: DataFormat, issueReq: IssueRequest, openId4VCIService: OpenId4VCIService) async throws -> WalletStorage.Document { - var dataToSave: Data = data - guard var ddt = DocDataType(rawValue: format.rawValue) else { throw WalletError(description: "Invalid format \(format.rawValue)") } - if var defTO = try? CodableCBORDecoder().decode(DeferredIssueModel.self, from: data) { - ddt = DocDataType.deferred - defTO.docType = docType; defTO.format = format.rawValue - dataToSave = try! CodableCBOREncoder().encode(defTO) + func finalizeIssuing(id: String, data: IssuanceOutcome, docType: String?, format: DataFormat, issueReq: IssueRequest, openId4VCIService: OpenId4VCIService) async throws -> WalletStorage.Document { + var dataToSave: Data + var docTypeToSave: String + guard let ddt = DocDataType(rawValue: format.rawValue) else { throw WalletError(description: "Invalid format \(format.rawValue)") } + switch data { + case .issued(let data): + dataToSave = data + let dt = if format == .cbor { IssuerSigned(data: [UInt8](data))?.issuerAuth.mso.docType ?? docType } else { docType } + guard let dt else { throw WalletError(description: "Unknown document type") } + docTypeToSave = dt + case .deferred(let deferredIssuanceModel): + dataToSave = try JSONEncoder().encode(deferredIssuanceModel) + docTypeToSave = "DEFERRED" } - let docTypeToSave = if format == .cbor { IssuerSigned(data: [UInt8](data))?.issuerAuth.mso.docType ?? docType } else { docType } - guard let docTypeToSave else { throw WalletError(description: "Unknown document type") } var issued: WalletStorage.Document if !openId4VCIService.usedSecureEnclave { issued = WalletStorage.Document(id: id, docType: docTypeToSave, docDataType: ddt, data: dataToSave, privateKeyType: .x963EncodedP256, privateKey: issueReq.keyData, createdAt: Date()) diff --git a/Sources/EudiWalletKit/Services/OpenId4VciService.swift b/Sources/EudiWalletKit/Services/OpenId4VciService.swift index 8c03538..7dba1ff 100644 --- a/Sources/EudiWalletKit/Services/OpenId4VciService.swift +++ b/Sources/EudiWalletKit/Services/OpenId4VciService.swift @@ -66,11 +66,10 @@ public class OpenId4VCIService: NSObject, ASWebAuthenticationPresentationContext /// - format: format of the exchanged data /// - useSecureEnclave: use secure enclave to protect the private key /// - Returns: The data of the document - public func issueDocument(docType: String, format: DataFormat, useSecureEnclave: Bool = true) async throws -> Data { + func issueDocument(docType: String, format: DataFormat, useSecureEnclave: Bool = true) async throws -> IssuanceOutcome { try initSecurityKeys(useSecureEnclave) - let str = try await issueByDocType(docType, format: format) - guard let data = Data(base64URLEncoded: str) else { throw OpenId4VCIError.dataNotValid } - return data + let res = try await issueByDocType(docType, format: format) + return res } /// Resolve issue offer and return available document metadata @@ -95,7 +94,7 @@ public class OpenId4VCIService: NSObject, ASWebAuthenticationPresentationContext try Issuer(authorizationServerMetadata: offer.authorizationServerMetadata, issuerMetadata: offer.credentialIssuerMetadata, config: config, parPoster: Poster(session: urlSession), tokenPoster: Poster(session: urlSession), requesterPoster: Poster(session: urlSession), deferredRequesterPoster: Poster(session: urlSession), notificationPoster: Poster(session: urlSession)) } - public func issueDocumentsByOfferUrl(offerUri: String, docTypes: [OfferedDocModel], txCodeValue: String?, format: DataFormat, useSecureEnclave: Bool = true, claimSet: ClaimSet? = nil) async throws -> [Data] { + func issueDocumentsByOfferUrl(offerUri: String, docTypes: [OfferedDocModel], txCodeValue: String?, format: DataFormat, useSecureEnclave: Bool = true, claimSet: ClaimSet? = nil) async throws -> [IssuanceOutcome] { guard format == .cbor else { throw fatalError("jwt format not implemented") } try initSecurityKeys(useSecureEnclave) guard let offer = Self.metadataCache[offerUri] else { throw WalletError(description: "offerUri not resolved. resolveOfferDocTypes must be called first")} @@ -110,9 +109,9 @@ public class OpenId4VCIService: NSObject, ASWebAuthenticationPresentationContext let data = await credentialInfo.asyncCompactMap { do { logger.info("Starting issuing with identifer \($0.identifier.value) and scope \($0.scope)") - let str = try await issueOfferedCredentialInternalValidated(authorized, offer: offer, issuer: issuer, credentialConfigurationIdentifier: $0.identifier, claimSet: claimSet) - logger.info("Credential str:\n\(str)") - return Data(base64URLEncoded: str) + let res = try await issueOfferedCredentialInternalValidated(authorized, offer: offer, issuer: issuer, credentialConfigurationIdentifier: $0.identifier, claimSet: claimSet) + // logger.info("Credential str:\n\(str)") + return res } catch { logger.error("Failed to issue document with scope \($0.scope)") logger.info("Exception: \(error)") @@ -123,7 +122,7 @@ public class OpenId4VCIService: NSObject, ASWebAuthenticationPresentationContext return data } - func issueByDocType(_ docType: String, format: DataFormat, claimSet: ClaimSet? = nil) async throws -> String { + func issueByDocType(_ docType: String, format: DataFormat, claimSet: ClaimSet? = nil) async throws -> IssuanceOutcome { let credentialIssuerIdentifier = try CredentialIssuerId(credentialIssuerURL) let issuerMetadata = await CredentialIssuerMetadataResolver(fetcher: Fetcher(session: urlSession)).resolve(source: .credentialIssuer(credentialIssuerIdentifier)) switch issuerMetadata { @@ -144,7 +143,7 @@ public class OpenId4VCIService: NSObject, ASWebAuthenticationPresentationContext } } - private func issueOfferedCredentialInternal(_ authorized: AuthorizedRequest, issuer: Issuer, credentialConfigurationIdentifier: CredentialConfigurationIdentifier, claimSet: ClaimSet?) async throws -> String { + private func issueOfferedCredentialInternal(_ authorized: AuthorizedRequest, issuer: Issuer, credentialConfigurationIdentifier: CredentialConfigurationIdentifier, claimSet: ClaimSet?) async throws -> IssuanceOutcome { switch authorized { case .noProofRequired: return try await noProofRequiredSubmissionUseCase(issuer: issuer, noProofRequiredState: authorized, credentialConfigurationIdentifier: credentialConfigurationIdentifier, claimSet: claimSet) @@ -153,7 +152,7 @@ public class OpenId4VCIService: NSObject, ASWebAuthenticationPresentationContext } } - private func issueOfferedCredentialInternalValidated(_ authorized: AuthorizedRequest, offer: CredentialOffer, issuer: Issuer, credentialConfigurationIdentifier: CredentialConfigurationIdentifier, claimSet: ClaimSet? = nil) async throws -> String { + private func issueOfferedCredentialInternalValidated(_ authorized: AuthorizedRequest, offer: CredentialOffer, issuer: Issuer, credentialConfigurationIdentifier: CredentialConfigurationIdentifier, claimSet: ClaimSet? = nil) async throws -> IssuanceOutcome { let issuerMetadata = offer.credentialIssuerMetadata guard issuerMetadata.credentialsSupported.keys.contains(where: { $0.value == credentialConfigurationIdentifier.value }) else { throw WalletError(description: "Cannot find credential identifier \(credentialConfigurationIdentifier.value) in offer") @@ -233,9 +232,9 @@ public class OpenId4VCIService: NSObject, ASWebAuthenticationPresentationContext throw WalletError(description: "Failed to get push authorization code request") } - private func noProofRequiredSubmissionUseCase(issuer: Issuer, noProofRequiredState: AuthorizedRequest, credentialConfigurationIdentifier: CredentialConfigurationIdentifier, claimSet: ClaimSet? = nil) async throws -> String { + private func noProofRequiredSubmissionUseCase(issuer: Issuer, noProofRequiredState: AuthorizedRequest, credentialConfigurationIdentifier: CredentialConfigurationIdentifier, claimSet: ClaimSet? = nil) async throws -> IssuanceOutcome { switch noProofRequiredState { - case .noProofRequired: + case .noProofRequired(let accessToken, let refreshToken, let credentialIdentifiers): let payload: IssuanceRequestPayload = .configurationBased(credentialConfigurationIdentifier: credentialConfigurationIdentifier, claimSet: claimSet) let responseEncryptionSpecProvider = { Issuer.createResponseEncryptionSpec($0) } let requestOutcome = try await issuer.requestSingle(noProofRequest: noProofRequiredState, requestPayload: payload, responseEncryptionSpecProvider: responseEncryptionSpecProvider) @@ -246,9 +245,12 @@ public class OpenId4VCIService: NSObject, ASWebAuthenticationPresentationContext if let result = response.credentialResponses.first { switch result { case .deferred(let transactionId): - return try await deferredCredentialUseCase(issuer: issuer, authorized: noProofRequiredState, transactionId: transactionId) + //return try await deferredCredentialUseCase(issuer: issuer, authorized: noProofRequiredState, transactionId: transactionId) + let deferredModel = DeferredIssuanceModel(credentialIssuerUrl: credentialIssuerURL, accessToken: accessToken, refreshToken: refreshToken, credentialIdentifiers: credentialIdentifiers) + return .deferred(deferredModel) case .issued(_, let credential, _): - return credential + guard let data = Data(base64URLEncoded: credential) else { throw WalletError(description: "Invalid credential") } + return .issued(data) } } else { throw WalletError(description: "No credential response results available") @@ -265,7 +267,8 @@ public class OpenId4VCIService: NSObject, ASWebAuthenticationPresentationContext } } - private func proofRequiredSubmissionUseCase(issuer: Issuer, authorized: AuthorizedRequest, credentialConfigurationIdentifier: CredentialConfigurationIdentifier?, claimSet: ClaimSet? = nil) async throws -> String { + private func proofRequiredSubmissionUseCase(issuer: Issuer, authorized: AuthorizedRequest, credentialConfigurationIdentifier: CredentialConfigurationIdentifier?, claimSet: ClaimSet? = nil) async throws -> IssuanceOutcome { + guard case .proofRequired(let accessToken, let refreshToken, let cNonce, let credentialIdentifiers) = authorized else { throw WalletError(description: "Unexpected AuthorizedRequest case") } guard let credentialConfigurationIdentifier else { throw WalletError(description: "Credential configuration identifier not found") } let payload: IssuanceRequestPayload = .configurationBased(credentialConfigurationIdentifier: credentialConfigurationIdentifier, claimSet: claimSet) let responseEncryptionSpecProvider = { Issuer.createResponseEncryptionSpec($0) } @@ -277,9 +280,12 @@ public class OpenId4VCIService: NSObject, ASWebAuthenticationPresentationContext if let result = response.credentialResponses.first { switch result { case .deferred(let transactionId): - return try await deferredCredentialUseCase(issuer: issuer, authorized: authorized, transactionId: transactionId) + //return try await deferredCredentialUseCase(issuer: issuer, authorized: authorized, transactionId: transactionId) + let deferredModel = DeferredIssuanceModel(credentialIssuerUrl: credentialIssuerURL, accessToken: accessToken, refreshToken: refreshToken, credentialIdentifiers: credentialIdentifiers) + return .deferred(deferredModel) case .issued(_, let credential, _): - return credential + guard let data = Data(base64URLEncoded: credential) else { throw WalletError(description: "Invalid credential") } + return .issued(data) } } else { throw WalletError(description: "No credential response results available") @@ -293,17 +299,26 @@ public class OpenId4VCIService: NSObject, ASWebAuthenticationPresentationContext } } - private func deferredCredentialUseCase(issuer: Issuer, authorized: AuthorizedRequest, transactionId: TransactionId) async throws -> String { + private func deferredCredentialUseCase(issuer: Issuer, authorized: AuthorizedRequest, transactionId: TransactionId) async throws -> IssuanceOutcome { logger.info("--> [ISSUANCE] Got a deferred issuance response from server with transaction_id \(transactionId.value). Retrying issuance...") let deferredRequestResponse = try await issuer.requestDeferredIssuance(proofRequest: authorized, transactionId: transactionId) switch deferredRequestResponse { case .success(let response): switch response { case .issued(_, let credential): - return credential + guard let data = Data(base64URLEncoded: credential) else { throw WalletError(description: "Invalid credential") } + return .issued(data) case .issuancePending(let transactionId): logger.info("Credential not ready yet. Try after \(transactionId.interval ?? 0)") - return (try! CodableCBOREncoder().encode(DeferredIssueModel(credentialIssuerUrl: credentialIssuerURL, accessToken: authorized.accessToken?.accessToken ?? "", refreshToken: nil, transactionId: transactionId.value))).base64URLEncodedString() + let deferredModel = switch authorized { + case .noProofRequired(let accessToken, let refreshToken, let credentialIdentifiers): + DeferredIssuanceModel(credentialIssuerUrl: credentialIssuerURL, accessToken: accessToken, refreshToken: refreshToken, credentialIdentifiers: credentialIdentifiers) + case .proofRequired(let accessToken, let refreshToken, _, let credentialIdentifiers): + DeferredIssuanceModel(credentialIssuerUrl: credentialIssuerURL, accessToken: accessToken, refreshToken: refreshToken, credentialIdentifiers: credentialIdentifiers) + } + guard let accessToken = authorized.accessToken else { throw WalletError(description: "No access token provided")} + + return .deferred(deferredModel) case .errored(_, let errorDescription): throw WalletError(description: "\(errorDescription ?? "Something went wrong with your deferred request response")") } diff --git a/Sources/EudiWalletKit/Services/StorageManager.swift b/Sources/EudiWalletKit/Services/StorageManager.swift index 7fa1455..ab58e6e 100644 --- a/Sources/EudiWalletKit/Services/StorageManager.swift +++ b/Sources/EudiWalletKit/Services/StorageManager.swift @@ -188,22 +188,6 @@ public class StorageManager: ObservableObject { } -public struct DeferredIssueModel: Codable { - let credentialIssuerUrl: String - var docType: String? - var format: String = DataFormat.cbor.rawValue - let accessToken: String - let refreshToken: String? - let transactionId: String - - public init(credentialIssuerUrl: String, docType: String? = nil, accessToken: String, refreshToken: String?, transactionId: String) { - self.credentialIssuerUrl = credentialIssuerUrl - self.docType = docType - self.accessToken = accessToken - self.refreshToken = refreshToken - self.transactionId = transactionId - } -} diff --git a/Sources/EudiWalletKit/ViewModels/DeferredIssueModel.swift b/Sources/EudiWalletKit/ViewModels/DeferredIssueModel.swift new file mode 100644 index 0000000..8ba9a36 --- /dev/null +++ b/Sources/EudiWalletKit/ViewModels/DeferredIssueModel.swift @@ -0,0 +1,33 @@ +/* +Copyright (c) 2023 European Commission + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import Foundation +import OpenID4VCI + +struct DeferredIssuanceModel: Codable { + let credentialIssuerUrl: String + let accessToken: IssuanceAccessToken + let refreshToken: IssuanceRefreshToken? + let credentialIdentifiers: AuthorizationDetailsIdentifiers? +} + +enum IssuanceOutcome { + case issued(Data) + case deferred(DeferredIssuanceModel) +} + + + From a3bd04114e88d9736acb467bd2cf4d291ca9743e Mon Sep 17 00:00:00 2001 From: Filippos Sakellaropoulos Date: Thu, 11 Jul 2024 02:52:52 +0300 Subject: [PATCH 03/12] include transactionId to deferred issue model --- Sources/EudiWalletKit/Services/OpenId4VciService.swift | 8 ++++---- Sources/EudiWalletKit/ViewModels/DeferredIssueModel.swift | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Sources/EudiWalletKit/Services/OpenId4VciService.swift b/Sources/EudiWalletKit/Services/OpenId4VciService.swift index 7dba1ff..9ab099c 100644 --- a/Sources/EudiWalletKit/Services/OpenId4VciService.swift +++ b/Sources/EudiWalletKit/Services/OpenId4VciService.swift @@ -246,7 +246,7 @@ public class OpenId4VCIService: NSObject, ASWebAuthenticationPresentationContext switch result { case .deferred(let transactionId): //return try await deferredCredentialUseCase(issuer: issuer, authorized: noProofRequiredState, transactionId: transactionId) - let deferredModel = DeferredIssuanceModel(credentialIssuerUrl: credentialIssuerURL, accessToken: accessToken, refreshToken: refreshToken, credentialIdentifiers: credentialIdentifiers) + let deferredModel = DeferredIssuanceModel(credentialIssuerUrl: credentialIssuerURL, accessToken: accessToken, refreshToken: refreshToken, credentialIdentifiers: credentialIdentifiers, transactionId: transactionId) return .deferred(deferredModel) case .issued(_, let credential, _): guard let data = Data(base64URLEncoded: credential) else { throw WalletError(description: "Invalid credential") } @@ -281,7 +281,7 @@ public class OpenId4VCIService: NSObject, ASWebAuthenticationPresentationContext switch result { case .deferred(let transactionId): //return try await deferredCredentialUseCase(issuer: issuer, authorized: authorized, transactionId: transactionId) - let deferredModel = DeferredIssuanceModel(credentialIssuerUrl: credentialIssuerURL, accessToken: accessToken, refreshToken: refreshToken, credentialIdentifiers: credentialIdentifiers) + let deferredModel = DeferredIssuanceModel(credentialIssuerUrl: credentialIssuerURL, accessToken: accessToken, refreshToken: refreshToken, credentialIdentifiers: credentialIdentifiers, transactionId: transactionId) return .deferred(deferredModel) case .issued(_, let credential, _): guard let data = Data(base64URLEncoded: credential) else { throw WalletError(description: "Invalid credential") } @@ -312,9 +312,9 @@ public class OpenId4VCIService: NSObject, ASWebAuthenticationPresentationContext logger.info("Credential not ready yet. Try after \(transactionId.interval ?? 0)") let deferredModel = switch authorized { case .noProofRequired(let accessToken, let refreshToken, let credentialIdentifiers): - DeferredIssuanceModel(credentialIssuerUrl: credentialIssuerURL, accessToken: accessToken, refreshToken: refreshToken, credentialIdentifiers: credentialIdentifiers) + DeferredIssuanceModel(credentialIssuerUrl: credentialIssuerURL, accessToken: accessToken, refreshToken: refreshToken, credentialIdentifiers: credentialIdentifiers, transactionId: transactionId) case .proofRequired(let accessToken, let refreshToken, _, let credentialIdentifiers): - DeferredIssuanceModel(credentialIssuerUrl: credentialIssuerURL, accessToken: accessToken, refreshToken: refreshToken, credentialIdentifiers: credentialIdentifiers) + DeferredIssuanceModel(credentialIssuerUrl: credentialIssuerURL, accessToken: accessToken, refreshToken: refreshToken, credentialIdentifiers: credentialIdentifiers, transactionId: transactionId) } guard let accessToken = authorized.accessToken else { throw WalletError(description: "No access token provided")} diff --git a/Sources/EudiWalletKit/ViewModels/DeferredIssueModel.swift b/Sources/EudiWalletKit/ViewModels/DeferredIssueModel.swift index 8ba9a36..0834de1 100644 --- a/Sources/EudiWalletKit/ViewModels/DeferredIssueModel.swift +++ b/Sources/EudiWalletKit/ViewModels/DeferredIssueModel.swift @@ -22,6 +22,7 @@ struct DeferredIssuanceModel: Codable { let accessToken: IssuanceAccessToken let refreshToken: IssuanceRefreshToken? let credentialIdentifiers: AuthorizationDetailsIdentifiers? + let transactionId: TransactionId } enum IssuanceOutcome { From 3449464b24a19ed7be853f9820727e4329c722e5 Mon Sep 17 00:00:00 2001 From: Filippos Sakellaropoulos Date: Thu, 11 Jul 2024 02:53:24 +0300 Subject: [PATCH 04/12] remove unused code --- Sources/EudiWalletKit/Services/OpenId4VciService.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Sources/EudiWalletKit/Services/OpenId4VciService.swift b/Sources/EudiWalletKit/Services/OpenId4VciService.swift index 9ab099c..283b3b7 100644 --- a/Sources/EudiWalletKit/Services/OpenId4VciService.swift +++ b/Sources/EudiWalletKit/Services/OpenId4VciService.swift @@ -316,8 +316,6 @@ public class OpenId4VCIService: NSObject, ASWebAuthenticationPresentationContext case .proofRequired(let accessToken, let refreshToken, _, let credentialIdentifiers): DeferredIssuanceModel(credentialIssuerUrl: credentialIssuerURL, accessToken: accessToken, refreshToken: refreshToken, credentialIdentifiers: credentialIdentifiers, transactionId: transactionId) } - guard let accessToken = authorized.accessToken else { throw WalletError(description: "No access token provided")} - return .deferred(deferredModel) case .errored(_, let errorDescription): throw WalletError(description: "\(errorDescription ?? "Something went wrong with your deferred request response")") From 854f69edacb476246fec9f64dbaba2f48932f74c Mon Sep 17 00:00:00 2001 From: Filippos Sakellaropoulos Date: Fri, 12 Jul 2024 23:54:47 +0300 Subject: [PATCH 05/12] - Update eudi-lib-ios-openid4vci-swift to version 0.3.2 - refactor deferred issuance model - use document-status property instead of boolean isdeferred --- Package.resolved | 4 +-- Package.swift | 2 +- README.md | 6 ++-- Sources/EudiWalletKit/EudiWallet.swift | 31 ++++++++++--------- .../Services/OpenId4VciService.swift | 28 ++++++++++------- .../Services/StorageManager.swift | 6 ++-- ...odel.swift => DeferredIssuanceModel.swift} | 7 +++-- ...Model.swift => OfferedIssuanceModel.swift} | 2 +- changelog.md | 11 +++++++ 9 files changed, 59 insertions(+), 38 deletions(-) rename Sources/EudiWalletKit/ViewModels/{DeferredIssueModel.swift => DeferredIssuanceModel.swift} (83%) rename Sources/EudiWalletKit/ViewModels/{OfferedDocModel.swift => OfferedIssuanceModel.swift} (97%) diff --git a/Package.resolved b/Package.resolved index cfedf62..476e669 100644 --- a/Package.resolved +++ b/Package.resolved @@ -50,8 +50,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-openid4vci-swift.git", "state" : { - "revision" : "727bc48dcbdb7cd1cd942c0086fb8e9b7dc06879", - "version" : "0.3.1" + "revision" : "9e512f934c9e3a62dbc342381ed94ce194881148", + "version" : "0.3.2" } }, { diff --git a/Package.swift b/Package.swift index 19c3fe3..c32a3ed 100644 --- a/Package.swift +++ b/Package.swift @@ -18,7 +18,7 @@ let package = Package( .package(url: "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-iso18013-data-transfer.git", exact: "0.2.9"), .package(url: "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-wallet-storage.git", .upToNextMajor(from: "0.2.0")), .package(url: "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-siop-openid4vp-swift.git", exact: "0.3.2"), - .package(url: "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-openid4vci-swift.git", exact: "0.3.1"), + .package(url: "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-openid4vci-swift.git", exact: "0.3.2"), ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. diff --git a/README.md b/README.md index 8dff4d4..6edc5d4 100644 --- a/README.md +++ b/README.md @@ -194,13 +194,13 @@ catch { #### Resolving Credential offer The library provides the `resolveOfferUrlDocTypes(uriOffer:)` method that resolves the credential offer URI. -The method returns the resolved `OfferedIssueModel` object that contains the offer's data (offered document types, issuer name and transaction code specification for pre-authorized flow). The offer's data can be displayed to the +The method returns the resolved `OfferedIssuanceModel` object that contains the offer's data (offered document types, issuer name and transaction code specification for pre-authorized flow). The offer's data can be displayed to the user. The following example shows how to resolve a credential offer: ```swift - func resolveOfferUrlDocTypes(uriOffer: String) async throws -> OfferedIssueModel { + func resolveOfferUrlDocTypes(uriOffer: String) async throws -> OfferedIssuanceModel { return try await wallet.resolveOfferUrlDocTypes(uriOffer: uriOffer) } ``` @@ -220,7 +220,7 @@ The user is redirected in an authorization web view to the issuer's authorizatio #### Pre-Authorization code flow When Issuer supports the pre-authorization code flow, the resolved offer will also contain the corresponding -information. Specifically, the `txCodeSpec` field in the `OfferedIssueModel` object will contain: +information. Specifically, the `txCodeSpec` field in the `OfferedIssuanceModel` object will contain: - The input mode, whether it is NUMERIC or TEXT - The expected length of the input diff --git a/Sources/EudiWalletKit/EudiWallet.swift b/Sources/EudiWalletKit/EudiWallet.swift index a8a8cdf..848725a 100644 --- a/Sources/EudiWalletKit/EudiWallet.swift +++ b/Sources/EudiWalletKit/EudiWallet.swift @@ -30,7 +30,9 @@ public final class EudiWallet: ObservableObject { public private(set) var storage: StorageManager var storageService: any WalletStorage.DataStorageService { storage.storageService } /// Instance of the wallet initialized with default parameters - public static private(set) var standard: EudiWallet = EudiWallet() + public static private(set) var standard: EudiWallet = try! EudiWallet() + /// The [access group](https://developer.apple.com/documentation/security/ksecattraccessgroup) that documents are stored in. + public var accessGroup: String? { didSet { storage.storageService.accessGroup = accessGroup } } /// Whether user authentication via biometrics or passcode is required before sending user data public var userAuthenticationRequired: Bool /// Trusted root certificates to validate the reader authentication certificate included in the proximity request @@ -53,7 +55,8 @@ public final class EudiWallet: ObservableObject { public var urlSession: URLSession /// Initialize a wallet instance. All parameters are optional. - public init(storageType: StorageType = .keyChain, serviceName: String = "eudiw", accessGroup: String? = nil, trustedReaderCertificates: [Data]? = nil, userAuthenticationRequired: Bool = true, verifierApiUri: String? = nil, openID4VciIssuerUrl: String? = nil, openID4VciClientId: String? = nil, openID4VciRedirectUri: String? = nil, urlSession: URLSession? = nil) { + public init(storageType: StorageType = .keyChain, serviceName: String = "eudiw", accessGroup: String? = nil, trustedReaderCertificates: [Data]? = nil, userAuthenticationRequired: Bool = true, verifierApiUri: String? = nil, openID4VciIssuerUrl: String? = nil, openID4VciClientId: String? = nil, openID4VciRedirectUri: String? = nil, urlSession: URLSession? = nil) throws { + guard !serviceName.contains(":") else { throw WalletError(description: "Not allowed service name, remove : character") } let keyChainObj = KeyChainStorageService(serviceName: serviceName, accessGroup: accessGroup) let storageService = switch storageType { case .keyChain:keyChainObj } storage = StorageManager(storageService: storageService) @@ -117,11 +120,11 @@ public final class EudiWallet: ObservableObject { } var issued: WalletStorage.Document if !openId4VCIService.usedSecureEnclave { - issued = WalletStorage.Document(id: id, docType: docTypeToSave, docDataType: ddt, data: dataToSave, privateKeyType: .x963EncodedP256, privateKey: issueReq.keyData, createdAt: Date()) + issued = WalletStorage.Document(id: id, docType: docTypeToSave, docDataType: ddt, data: dataToSave, privateKeyType: .x963EncodedP256, privateKey: issueReq.keyData, createdAt: Date(), status: data.isDeferred ? .deferred : .issued) } else { - issued = WalletStorage.Document(id: id, docType: docTypeToSave, docDataType: ddt, data: dataToSave, privateKeyType: .secureEnclaveP256, privateKey: issueReq.keyData, createdAt: Date()) + issued = WalletStorage.Document(id: id, docType: docTypeToSave, docDataType: ddt, data: dataToSave, privateKeyType: .secureEnclaveP256, privateKey: issueReq.keyData, createdAt: Date(), status: data.isDeferred ? .deferred : .issued) } - try issueReq.saveToStorage(storage.storageService) + try issueReq.saveToStorage(storage.storageService, status: data.isDeferred ? .deferred : .issued) try endIssueDocument(issued) await storage.appendDocModel(issued) await storage.refreshPublishedVars() @@ -134,7 +137,7 @@ public final class EudiWallet: ObservableObject { /// - format: data format /// - useSecureEnclave: whether to use secure enclave (if supported) /// - Returns: Offered issue information model - public func resolveOfferUrlDocTypes(uriOffer: String, format: DataFormat = .cbor, useSecureEnclave: Bool = true) async throws -> OfferedIssueModel { + public func resolveOfferUrlDocTypes(uriOffer: String, format: DataFormat = .cbor, useSecureEnclave: Bool = true) async throws -> OfferedIssuanceModel { let (_, openId4VCIService, _) = try await prepareIssuing(docType: nil) return try await openId4VCIService.resolveOfferDocTypes(uriOffer: uriOffer, format: format) } @@ -166,17 +169,16 @@ public final class EudiWallet: ObservableObject { /// - Parameters: /// - id: Document identifier /// - issuer: Issuer function - public func beginIssueDocument(id: String, privateKeyType: PrivateKeyType = .secureEnclaveP256, saveToStorage: Bool = true) async throws -> IssueRequest { + public func beginIssueDocument(id: String, privateKeyType: PrivateKeyType = .secureEnclaveP256, saveToStorage: Bool = true, bDeferred: Bool = false) async throws -> IssueRequest { let request = try IssueRequest(id: id, privateKeyType: privateKeyType) - if saveToStorage { try request.saveToStorage(storage.storageService) } + if saveToStorage { try request.saveToStorage(storage.storageService, status: bDeferred ? .deferred : .issued) } return request } /// End issuing by saving the issuing document (and its private key) in storage /// - Parameter issued: The issued document public func endIssueDocument(_ issued: WalletStorage.Document) throws { - try storage.storageService.saveDocumentData(issued, dataToSaveType: .doc, dataType: issued.docDataType.rawValue, allowOverwrite: true) - try storage.storageService.saveDocumentData(issued, dataToSaveType: .key, dataType: issued.privateKeyType!.rawValue, allowOverwrite: true) + try storage.storageService.saveDocument(issued, allowOverwrite: true) } /// Load documents from storage @@ -187,6 +189,7 @@ public final class EudiWallet: ObservableObject { return try await storage.loadDocuments() } + /// Delete all documents from storage /// /// Calls ``storage`` loadDocuments @@ -200,10 +203,10 @@ public final class EudiWallet: ObservableObject { /// The mdoc data are stored in wallet storage as documents /// - Parameter sampleDataFiles: Names of sample files provided in the app bundle public func loadSampleData(sampleDataFiles: [String]? = nil) async throws { - try? storageService.deleteDocuments() + try? storageService.deleteDocuments(status: .issued) let docSamples = (sampleDataFiles ?? ["EUDI_sample_data"]).compactMap { Data(name:$0) } .compactMap(SignUpResponse.decomposeCBORSignupResponse(data:)).flatMap {$0} - .map { Document(docType: $0.docType, docDataType: .cbor, data: $0.issData, privateKeyType: .x963EncodedP256, privateKey: $0.pkData, createdAt: Date.distantPast, modifiedAt: nil) } + .map { Document(docType: $0.docType, docDataType: .cbor, data: $0.issData, privateKeyType: .x963EncodedP256, privateKey: $0.pkData, createdAt: Date.distantPast, modifiedAt: nil, status: .issued) } do { for docSample in docSamples { try storageService.saveDocument(docSample, allowOverwrite: true) @@ -220,11 +223,11 @@ public final class EudiWallet: ObservableObject { /// - docType: docType of documents to present (optional) /// - dataFormat: Exchanged data ``Format`` type /// - Returns: A data dictionary that can be used to initialize a presentation service - public func prepareServiceDataParameters(docType: String? = nil, dataFormat: DataFormat = .cbor ) throws -> [String : Any] { + func prepareServiceDataParameters(docType: String? = nil, dataFormat: DataFormat = .cbor ) throws -> [String: Any] { var parameters: [String: Any] switch dataFormat { case .cbor: - guard var docs = try storageService.loadDocuments(), docs.count > 0 else { throw WalletError(description: "No documents found") } + guard var docs = try storageService.loadDocuments(status: .issued), docs.count > 0 else { throw WalletError(description: "No documents found") } if let docType { docs = docs.filter { $0.docType == docType} } if let docType { guard docs.count > 0 else { throw WalletError(description: "No documents of type \(docType) found") } } let cborsWithKeys = docs.compactMap { $0.getCborData() } diff --git a/Sources/EudiWalletKit/Services/OpenId4VciService.swift b/Sources/EudiWalletKit/Services/OpenId4VciService.swift index 283b3b7..29e0d8a 100644 --- a/Sources/EudiWalletKit/Services/OpenId4VciService.swift +++ b/Sources/EudiWalletKit/Services/OpenId4VciService.swift @@ -77,23 +77,27 @@ public class OpenId4VCIService: NSObject, ASWebAuthenticationPresentationContext /// - uriOffer: Uri of the offer (from a QR or a deep link) /// - format: format of the exchanged data /// - Returns: The data of the document - public func resolveOfferDocTypes(uriOffer: String, format: DataFormat = .cbor) async throws -> OfferedIssueModel { + public func resolveOfferDocTypes(uriOffer: String, format: DataFormat = .cbor) async throws -> OfferedIssuanceModel { let result = await CredentialOfferRequestResolver(fetcher: Fetcher(session: urlSession), credentialIssuerMetadataResolver: CredentialIssuerMetadataResolver(fetcher: Fetcher(session: urlSession)), authorizationServerMetadataResolver: AuthorizationServerMetadataResolver(oidcFetcher: Fetcher(session: urlSession), oauthFetcher: Fetcher(session: urlSession))).resolve(source: try .init(urlString: uriOffer)) switch result { case .success(let offer): - let code: Grants.PreAuthorizedCode? = switch offer.grants { case .preAuthorizedCode(let preAuthorizedCode): preAuthorizedCode; case .both(_, let preAuthorizedCode): preAuthorizedCode; case .authorizationCode(_), .none: nil } + let code: Grants.PreAuthorizedCode? = switch offer.grants { case .preAuthorizedCode(let preAuthorizedCode): preAuthorizedCode; case .both(_, let preAuthorizedCode): preAuthorizedCode; case .authorizationCode(_), .none: nil } Self.metadataCache[uriOffer] = offer let credentialInfo = try getCredentialIdentifiers(credentialsSupported: offer.credentialIssuerMetadata.credentialsSupported.filter { offer.credentialConfigurationIdentifiers.contains($0.key) }, format: format) - return OfferedIssueModel(issuerName: offer.credentialIssuerIdentifier.url.absoluteString, docModels: credentialInfo.map(\.offered), txCodeSpec: code?.txCode) + return OfferedIssuanceModel(issuerName: offer.credentialIssuerIdentifier.url.absoluteString, docModels: credentialInfo.map(\.offered), txCodeSpec: code?.txCode) case .failure(let error): throw WalletError(description: "Unable to resolve credential offer: \(error.localizedDescription)") } } - public func getIssuer(offer: CredentialOffer) throws -> Issuer { + func getIssuer(offer: CredentialOffer) throws -> Issuer { try Issuer(authorizationServerMetadata: offer.authorizationServerMetadata, issuerMetadata: offer.credentialIssuerMetadata, config: config, parPoster: Poster(session: urlSession), tokenPoster: Poster(session: urlSession), requesterPoster: Poster(session: urlSession), deferredRequesterPoster: Poster(session: urlSession), notificationPoster: Poster(session: urlSession)) } + func getIssuerForDeferred(data: DeferredIssuanceModel) throws -> Issuer { + try Issuer(authorizationServerMetadata: .oauth(AuthorizationServerMetadata(issuer: nil, authorizationEndpoint: nil, tokenEndpoint: nil, introspectionEndpoint: nil, jwksURI: nil, grantTypesSupported: nil, responseTypesSupported: nil, requestObjectSigningAlgValuesSupported: nil, requestObjectEncryptionAlgValuesSupported: nil, requestObjectEncryptionEncValuesSupported: nil, responseModesSupported: nil, registrationEndpoint: nil, tokenEndpointAuthMethodsSupported: nil, tokenEndpointAuthSigningAlgValuesSupported: nil, introspectionEndpointAuthMethodsSupported: nil, introspectionEndpointAuthSigningAlgValuesSupported: nil, authorizationSigningAlgValuesSupported: nil, authorizationEncryptionAlgValuesSupported: nil, authorizationEncryptionEncValuesSupported: nil, scopesSupported: nil, requestParameterSupported: nil, requestURIParameterSupported: nil, requireRequestURIRegistration: nil, codeChallengeMethodsSupported: nil, tlsClientCertificateBoundAccessTokens: nil, dpopSigningAlgValuesSupported: nil, revocationEndpoint: nil, revocationEndpointAuthMethodsSupported: nil, revocationEndpointAuthSigningAlgValuesSupported: nil, deviceAuthorizationEndpoint: nil, backchannelTokenDeliveryModesSupported: nil, backchannelAuthenticationEndpoint: nil, backchannelAuthenticationRequestSigningAlgValuesSupported: nil, requirePushedAuthorizationRequests: nil, pushedAuthorizationRequestEndpoint: nil, mtlsEndpointAliases: nil, authorizationResponseIssParameterSupported: nil)), issuerMetadata: CredentialIssuerMetadata(credentialIssuerIdentifier: CredentialIssuerId(""), authorizationServers: [], credentialEndpoint: data.deferredCredentialEndpoint, batchCredentialEndpoint: nil, deferredCredentialEndpoint: data.deferredCredentialEndpoint, notificationEndpoint: nil, credentialConfigurationsSupported: [:], signedMetadata: nil, display: nil), config: config, parPoster: Poster(session: urlSession), tokenPoster: Poster(session: urlSession), requesterPoster: Poster(session: urlSession), deferredRequesterPoster: Poster(session: urlSession), notificationPoster: Poster(session: urlSession)) + } + func issueDocumentsByOfferUrl(offerUri: String, docTypes: [OfferedDocModel], txCodeValue: String?, format: DataFormat, useSecureEnclave: Bool = true, claimSet: ClaimSet? = nil) async throws -> [IssuanceOutcome] { guard format == .cbor else { throw fatalError("jwt format not implemented") } try initSecurityKeys(useSecureEnclave) @@ -105,7 +109,7 @@ public class OpenId4VCIService: NSObject, ASWebAuthenticationPresentationContext let preAuthorizedCode: String? = code?.preAuthorizedCode let issuer = try getIssuer(offer: offer) if preAuthorizedCode != nil && txCodeSpec != nil && txCodeValue == nil { throw WalletError(description: "A transaction code is required for this offer") } - let authorized = if let preAuthorizedCode, let txCodeValue, let authCode = try? IssuanceAuthorization(preAuthorizationCode: preAuthorizedCode, txCode: txCodeSpec) { try await issuer.authorizeWithPreAuthorizationCode(credentialOffer: offer, authorizationCode: authCode, clientId: config.clientId, transactionCode: txCodeValue).get() } else { try await authorizeRequestWithAuthCodeUseCase(issuer: issuer, offer: offer) } + let authorized = if let preAuthorizedCode, let authCode = try? IssuanceAuthorization(preAuthorizationCode: preAuthorizedCode, txCode: txCodeSpec) { try await issuer.authorizeWithPreAuthorizationCode(credentialOffer: offer, authorizationCode: authCode, clientId: config.clientId, transactionCode: txCodeValue).get() } else { try await authorizeRequestWithAuthCodeUseCase(issuer: issuer, offer: offer) } let data = await credentialInfo.asyncCompactMap { do { logger.info("Starting issuing with identifer \($0.identifier.value) and scope \($0.scope)") @@ -234,7 +238,7 @@ public class OpenId4VCIService: NSObject, ASWebAuthenticationPresentationContext private func noProofRequiredSubmissionUseCase(issuer: Issuer, noProofRequiredState: AuthorizedRequest, credentialConfigurationIdentifier: CredentialConfigurationIdentifier, claimSet: ClaimSet? = nil) async throws -> IssuanceOutcome { switch noProofRequiredState { - case .noProofRequired(let accessToken, let refreshToken, let credentialIdentifiers): + case .noProofRequired(let accessToken, let refreshToken, _): let payload: IssuanceRequestPayload = .configurationBased(credentialConfigurationIdentifier: credentialConfigurationIdentifier, claimSet: claimSet) let responseEncryptionSpecProvider = { Issuer.createResponseEncryptionSpec($0) } let requestOutcome = try await issuer.requestSingle(noProofRequest: noProofRequiredState, requestPayload: payload, responseEncryptionSpecProvider: responseEncryptionSpecProvider) @@ -246,7 +250,7 @@ public class OpenId4VCIService: NSObject, ASWebAuthenticationPresentationContext switch result { case .deferred(let transactionId): //return try await deferredCredentialUseCase(issuer: issuer, authorized: noProofRequiredState, transactionId: transactionId) - let deferredModel = DeferredIssuanceModel(credentialIssuerUrl: credentialIssuerURL, accessToken: accessToken, refreshToken: refreshToken, credentialIdentifiers: credentialIdentifiers, transactionId: transactionId) + let deferredModel = await DeferredIssuanceModel(deferredCredentialEndpoint: issuer.issuerMetadata.deferredCredentialEndpoint!, accessToken: accessToken, refreshToken: refreshToken, transactionId: transactionId) return .deferred(deferredModel) case .issued(_, let credential, _): guard let data = Data(base64URLEncoded: credential) else { throw WalletError(description: "Invalid credential") } @@ -281,7 +285,7 @@ public class OpenId4VCIService: NSObject, ASWebAuthenticationPresentationContext switch result { case .deferred(let transactionId): //return try await deferredCredentialUseCase(issuer: issuer, authorized: authorized, transactionId: transactionId) - let deferredModel = DeferredIssuanceModel(credentialIssuerUrl: credentialIssuerURL, accessToken: accessToken, refreshToken: refreshToken, credentialIdentifiers: credentialIdentifiers, transactionId: transactionId) + let deferredModel = await DeferredIssuanceModel(deferredCredentialEndpoint: issuer.issuerMetadata.deferredCredentialEndpoint!, accessToken: accessToken, refreshToken: refreshToken, transactionId: transactionId) return .deferred(deferredModel) case .issued(_, let credential, _): guard let data = Data(base64URLEncoded: credential) else { throw WalletError(description: "Invalid credential") } @@ -311,10 +315,10 @@ public class OpenId4VCIService: NSObject, ASWebAuthenticationPresentationContext case .issuancePending(let transactionId): logger.info("Credential not ready yet. Try after \(transactionId.interval ?? 0)") let deferredModel = switch authorized { - case .noProofRequired(let accessToken, let refreshToken, let credentialIdentifiers): - DeferredIssuanceModel(credentialIssuerUrl: credentialIssuerURL, accessToken: accessToken, refreshToken: refreshToken, credentialIdentifiers: credentialIdentifiers, transactionId: transactionId) - case .proofRequired(let accessToken, let refreshToken, _, let credentialIdentifiers): - DeferredIssuanceModel(credentialIssuerUrl: credentialIssuerURL, accessToken: accessToken, refreshToken: refreshToken, credentialIdentifiers: credentialIdentifiers, transactionId: transactionId) + case .noProofRequired(let accessToken, let refreshToken, _): + await DeferredIssuanceModel(deferredCredentialEndpoint: issuer.issuerMetadata.deferredCredentialEndpoint!, accessToken: accessToken, refreshToken: refreshToken, transactionId: transactionId) + case .proofRequired(let accessToken, let refreshToken, _, _): + await DeferredIssuanceModel(deferredCredentialEndpoint: issuer.issuerMetadata.deferredCredentialEndpoint!, accessToken: accessToken, refreshToken: refreshToken, transactionId: transactionId) } return .deferred(deferredModel) case .errored(_, let errorDescription): diff --git a/Sources/EudiWalletKit/Services/StorageManager.swift b/Sources/EudiWalletKit/Services/StorageManager.swift index ab58e6e..8d9452b 100644 --- a/Sources/EudiWalletKit/Services/StorageManager.swift +++ b/Sources/EudiWalletKit/Services/StorageManager.swift @@ -95,7 +95,7 @@ public class StorageManager: ObservableObject { /// - Returns: An array of ``WalletStorage.Document`` objects @discardableResult public func loadDocuments() async throws -> [WalletStorage.Document]? { do { - guard let docs = try storageService.loadDocuments() else { return nil } + guard let docs = try storageService.loadDocuments(status: .issued) else { return nil } await refreshDocModels(docs) await refreshPublishedVars() return docs @@ -156,7 +156,7 @@ public class StorageManager: ObservableObject { guard index < documentIds.count else { return } let id = mdocModels[index].id do { - try storageService.deleteDocument(id: id) + try storageService.deleteDocument(id: id, status: .issued) await MainActor.run { if docTypes[index] == IsoMdlModel.isoDocType { mdlModel = nil } if docTypes[index] == EuPidModel.euPidDocType { pidModel = nil } @@ -172,7 +172,7 @@ public class StorageManager: ObservableObject { /// Delete documenmts public func deleteDocuments() async throws { do { - try storageService.deleteDocuments() + try storageService.deleteDocuments(status: .issued) await MainActor.run { mdocModels = []; mdlModel = nil; pidModel = nil } await refreshPublishedVars() } catch { diff --git a/Sources/EudiWalletKit/ViewModels/DeferredIssueModel.swift b/Sources/EudiWalletKit/ViewModels/DeferredIssuanceModel.swift similarity index 83% rename from Sources/EudiWalletKit/ViewModels/DeferredIssueModel.swift rename to Sources/EudiWalletKit/ViewModels/DeferredIssuanceModel.swift index 0834de1..bbc9f68 100644 --- a/Sources/EudiWalletKit/ViewModels/DeferredIssueModel.swift +++ b/Sources/EudiWalletKit/ViewModels/DeferredIssuanceModel.swift @@ -18,10 +18,9 @@ import Foundation import OpenID4VCI struct DeferredIssuanceModel: Codable { - let credentialIssuerUrl: String + let deferredCredentialEndpoint: CredentialIssuerEndpoint let accessToken: IssuanceAccessToken let refreshToken: IssuanceRefreshToken? - let credentialIdentifiers: AuthorizationDetailsIdentifiers? let transactionId: TransactionId } @@ -30,5 +29,9 @@ enum IssuanceOutcome { case deferred(DeferredIssuanceModel) } +extension IssuanceOutcome { + var isDeferred: Bool { switch self { case .deferred(_): true; default: false } } +} + diff --git a/Sources/EudiWalletKit/ViewModels/OfferedDocModel.swift b/Sources/EudiWalletKit/ViewModels/OfferedIssuanceModel.swift similarity index 97% rename from Sources/EudiWalletKit/ViewModels/OfferedDocModel.swift rename to Sources/EudiWalletKit/ViewModels/OfferedIssuanceModel.swift index b0587e5..23b4ee5 100644 --- a/Sources/EudiWalletKit/ViewModels/OfferedDocModel.swift +++ b/Sources/EudiWalletKit/ViewModels/OfferedIssuanceModel.swift @@ -20,7 +20,7 @@ import OpenID4VCI /// Offered issue model contains information gathered by resolving an issue offer URL. /// /// This information is returned from ``EudiWallet/resolveOfferUrlDocTypes(uriOffer:format:useSecureEnclave:)`` -public struct OfferedIssueModel { +public struct OfferedIssuanceModel { /// Issuer name (currently the URL) public let issuerName: String /// Document types included in the offer diff --git a/changelog.md b/changelog.md index 9fedc8b..8a12648 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,14 @@ +## v0.5.7 +- Update eudi-lib-ios-openid4vci-swift to version 0.3.2 +- Rename `OfferedIssueModel` to `OfferedIssuanceModel` +- `EudiWallet`: added property `public var accessGroup: String?` (used for sharing keychain items between apps with the same access group) + +## v0.5.6 +- Update eudi-lib-ios-siop-openid4vp-swift to version 0.3.2 + +## v0.5.5 +- Update eudi-lib-ios-openid4vci-swift to version 0.3.1 + ## v0.5.4 ### Custom URLSession variable - Added `public var urlSession: URLSession` variable to `EudiWallet` class. This variable can be used to set a custom URLSession for network requests. Allows for custom configuration of the URLSession, such as setting a custom timeout interval or Self-Signed certificates. From e07b780715f9c071539f21ec85eadcc37b249bc0 Mon Sep 17 00:00:00 2001 From: Filippos Sakellaropoulos Date: Sat, 13 Jul 2024 00:55:48 +0300 Subject: [PATCH 06/12] use private(set) for storage-manager public variables --- .../Services/StorageManager.swift | 39 ++++++++----------- changelog.md | 1 + 2 files changed, 17 insertions(+), 23 deletions(-) diff --git a/Sources/EudiWalletKit/Services/StorageManager.swift b/Sources/EudiWalletKit/Services/StorageManager.swift index 8d9452b..ee7bab8 100644 --- a/Sources/EudiWalletKit/Services/StorageManager.swift +++ b/Sources/EudiWalletKit/Services/StorageManager.swift @@ -24,25 +24,19 @@ import CryptoKit /// Storage manager. Provides services and view models public class StorageManager: ObservableObject { public static let knownDocTypes = [EuPidModel.euPidDocType, IsoMdlModel.isoDocType] - /// Array of doc.types of documents loaded in the wallet - public var docTypes: [String] { mdocModels.map(\.docType) } /// Array of document models loaded in the wallet - @Published public var mdocModels: [any MdocDecodable] = [] - /// Array of document identifiers loaded in the wallet - public var documentIds: [String] { mdocModels.map(\.id) } + @Published public private(set) var mdocModels: [any MdocDecodable] = [] var storageService: any DataStorageService /// Whether wallet currently has loaded data - @Published public var hasData: Bool = false + @Published public private(set) var hasData: Bool = false /// Whether wallet currently has loaded a document with doc.type included in the ``knownDocTypes`` array - @Published public var hasWellKnownData: Bool = false + @Published public private(set) var hasWellKnownData: Bool = false /// Count of documents loaded in the wallet - @Published public var docCount: Int = 0 + @Published public private(set) var docCount: Int = 0 /// The first driver license model loaded in the wallet (deprecated) - @Published public var mdlModel: IsoMdlModel? + @Published public private(set) var mdlModel: IsoMdlModel? /// The first PID model loaded in the wallet (deprecated) - @Published public var pidModel: EuPidModel? - /// Other document models loaded in the wallet - @Published public var otherModels: [GenericMdocModel] = [] + @Published public private(set) var pidModel: EuPidModel? /// Error object with localized message @Published public var uiError: WalletError? let logger: Logger @@ -55,11 +49,10 @@ public class StorageManager: ObservableObject { @MainActor func refreshPublishedVars() { hasData = mdocModels.count > 0 - hasWellKnownData = hasData && !Set(docTypes).isDisjoint(with: Self.knownDocTypes) + hasWellKnownData = hasData && !Set(mdocModels.map(\.docType)).isDisjoint(with: Self.knownDocTypes) docCount = mdocModels.count mdlModel = getTypedDoc() pidModel = getTypedDoc() - otherModels = getTypedDocs() } @MainActor @@ -116,7 +109,7 @@ public class StorageManager: ObservableObject { /// Get document model by index /// - Parameter index: Index in array of loaded models /// - Returns: The ``MdocDecodable`` model - public func getDocumentModel(index: Int) -> (any MdocDecodable)? { + func getDocumentModel(index: Int) -> (any MdocDecodable)? { guard index < mdocModels.count else { return nil } return mdocModels[index] } @@ -125,7 +118,7 @@ public class StorageManager: ObservableObject { /// - Parameter id: The id of the document model to return /// - Returns: The ``MdocDecodable`` model public func getDocumentModel(id: String) -> (any MdocDecodable)? { - guard let i = documentIds.firstIndex(of: id) else { return nil } + guard let i = mdocModels.map(\.id).firstIndex(of: id) else { return nil } return getDocumentModel(index: i) } @@ -133,8 +126,8 @@ public class StorageManager: ObservableObject { /// - Parameter docType: The docType of the document model to return /// - Returns: The ``MdocDecodable`` model public func getDocumentModels(docType: String) -> [any MdocDecodable] { - return (0...Index = documentIds.firstIndex(of: id) else { return } + guard let i: Array.Index = mdocModels.map(\.id).firstIndex(of: id) else { return } do { try await deleteDocument(index: i) } catch { @@ -152,14 +145,14 @@ public class StorageManager: ObservableObject { } /// Delete document by Index /// - Parameter index: Index in array of loaded models - public func deleteDocument(index: Int) async throws { - guard index < documentIds.count else { return } + func deleteDocument(index: Int) async throws { + guard index < mdocModels.count else { return } let id = mdocModels[index].id do { try storageService.deleteDocument(id: id, status: .issued) await MainActor.run { - if docTypes[index] == IsoMdlModel.isoDocType { mdlModel = nil } - if docTypes[index] == EuPidModel.euPidDocType { pidModel = nil } + if mdocModels[index].docType == IsoMdlModel.isoDocType { mdlModel = nil } + if mdocModels[index].docType == EuPidModel.euPidDocType { pidModel = nil } mdocModels.remove(at: index) } await refreshPublishedVars() diff --git a/changelog.md b/changelog.md index 8a12648..2dc1226 100644 --- a/changelog.md +++ b/changelog.md @@ -2,6 +2,7 @@ - Update eudi-lib-ios-openid4vci-swift to version 0.3.2 - Rename `OfferedIssueModel` to `OfferedIssuanceModel` - `EudiWallet`: added property `public var accessGroup: String?` (used for sharing keychain items between apps with the same access group) +- StorageManager: remove `otherModels`, `docTypes`, `documentIds` properties ## v0.5.6 - Update eudi-lib-ios-siop-openid4vp-swift to version 0.3.2 From 77c81f5c5f47b7a63877524d18c7173c736e815a Mon Sep 17 00:00:00 2001 From: Filippos Sakellaropoulos Date: Sun, 14 Jul 2024 01:06:10 +0300 Subject: [PATCH 07/12] implement requestDeferredIssuance --- Sources/EudiWalletKit/EudiWallet.swift | 43 ++++++++++++----- .../EudiWalletKit/Services/Enumerations.swift | 11 +++++ .../Services/OpenId4VciService.swift | 7 +++ .../Services/StorageManager.swift | 48 ++++++++++++------- changelog.md | 6 ++- 5 files changed, 86 insertions(+), 29 deletions(-) diff --git a/Sources/EudiWalletKit/EudiWallet.swift b/Sources/EudiWalletKit/EudiWallet.swift index 848725a..05cdfc5 100644 --- a/Sources/EudiWalletKit/EudiWallet.swift +++ b/Sources/EudiWalletKit/EudiWallet.swift @@ -56,7 +56,7 @@ public final class EudiWallet: ObservableObject { /// Initialize a wallet instance. All parameters are optional. public init(storageType: StorageType = .keyChain, serviceName: String = "eudiw", accessGroup: String? = nil, trustedReaderCertificates: [Data]? = nil, userAuthenticationRequired: Bool = true, verifierApiUri: String? = nil, openID4VciIssuerUrl: String? = nil, openID4VciClientId: String? = nil, openID4VciRedirectUri: String? = nil, urlSession: URLSession? = nil) throws { - guard !serviceName.contains(":") else { throw WalletError(description: "Not allowed service name, remove : character") } + guard !serviceName.isEmpty, !serviceName.contains(":") else { throw WalletError(description: "Not allowed service name, remove : character") } let keyChainObj = KeyChainStorageService(serviceName: serviceName, accessGroup: accessGroup) let storageService = switch storageType { case .keyChain:keyChainObj } storage = StorageManager(storageService: storageService) @@ -104,6 +104,16 @@ public final class EudiWallet: ObservableObject { return try await finalizeIssuing(id: id, data: data, docType: docType, format: format, issueReq: issueReq, openId4VCIService: openId4VCIService) } + + @discardableResult public func requestDeferredIssuance(deferredDoc: WalletStorage.Document) async throws -> WalletStorage.Document { + guard deferredDoc.status == .deferred else { throw WalletError(description: "Invalid document status") } + guard let pkt = deferredDoc.privateKeyType, let pk = deferredDoc.privateKey, let format = DataFormat(deferredDoc.docDataType) else { throw WalletError(description: "Invalid document") } + let issueReq = try IssueRequest(id: deferredDoc.id, docType: deferredDoc.docType, privateKeyType: pkt, keyData: pk) + let openId4VCIService = OpenId4VCIService(issueRequest: issueReq, credentialIssuerURL: "", clientId: "", callbackScheme: openID4VciRedirectUri, urlSession: urlSession) + let data = try await openId4VCIService.requestDeferredIssuance(deferredDoc: deferredDoc) + return try await finalizeIssuing(id: deferredDoc.id, data: data, docType: deferredDoc.docType, format: format, issueReq: issueReq, openId4VCIService: openId4VCIService) + } + func finalizeIssuing(id: String, data: IssuanceOutcome, docType: String?, format: DataFormat, issueReq: IssueRequest, openId4VCIService: OpenId4VCIService) async throws -> WalletStorage.Document { var dataToSave: Data var docTypeToSave: String @@ -116,19 +126,23 @@ public final class EudiWallet: ObservableObject { docTypeToSave = dt case .deferred(let deferredIssuanceModel): dataToSave = try JSONEncoder().encode(deferredIssuanceModel) - docTypeToSave = "DEFERRED" + docTypeToSave = docType ?? "DEFERRED" } - var issued: WalletStorage.Document + var newDocument: WalletStorage.Document + let newDocStatus: WalletStorage.DocumentStatus = data.isDeferred ? .deferred : .issued if !openId4VCIService.usedSecureEnclave { - issued = WalletStorage.Document(id: id, docType: docTypeToSave, docDataType: ddt, data: dataToSave, privateKeyType: .x963EncodedP256, privateKey: issueReq.keyData, createdAt: Date(), status: data.isDeferred ? .deferred : .issued) + newDocument = WalletStorage.Document(id: id, docType: docTypeToSave, docDataType: ddt, data: dataToSave, privateKeyType: .x963EncodedP256, privateKey: issueReq.keyData, createdAt: Date(), status: newDocStatus) } else { - issued = WalletStorage.Document(id: id, docType: docTypeToSave, docDataType: ddt, data: dataToSave, privateKeyType: .secureEnclaveP256, privateKey: issueReq.keyData, createdAt: Date(), status: data.isDeferred ? .deferred : .issued) + newDocument = WalletStorage.Document(id: id, docType: docTypeToSave, docDataType: ddt, data: dataToSave, privateKeyType: .secureEnclaveP256, privateKey: issueReq.keyData, createdAt: Date(), status: newDocStatus) } - try issueReq.saveToStorage(storage.storageService, status: data.isDeferred ? .deferred : .issued) - try endIssueDocument(issued) - await storage.appendDocModel(issued) + try issueReq.saveToStorage(storage.storageService, status: newDocStatus) + try endIssueDocument(newDocument) + await storage.appendDocModel(newDocument) await storage.refreshPublishedVars() - return issued + if !data.isDeferred, let index = storage.deferredDocuments.firstIndex(where: { $0.id == id }) { + try await storage.deleteDocument(index: index) + } + return newDocument } /// Resolve OpenID4VCI offer URL document types. Resolved offer metadata are cached @@ -160,10 +174,15 @@ public final class EudiWallet: ObservableObject { for (i, docData) in docsData.enumerated() { if i > 0 { (issueReq, openId4VCIService, id) = try await prepareIssuing(docType: nil) } openId4VCIService.usedSecureEnclave = useSecureEnclave && SecureEnclave.isAvailable - documents.append(try await finalizeIssuing(id: id, data: docData, docType: nil, format: format, issueReq: issueReq, openId4VCIService: openId4VCIService)) + documents.append(try await finalizeIssuing(id: id, data: docData, docType: docData.isDeferred ? docTypes[i].docType : nil, format: format, issueReq: issueReq, openId4VCIService: openId4VCIService)) } return documents } + + public func re() { + + } + /// Begin issuing a document by generating an issue request /// /// - Parameters: @@ -185,8 +204,8 @@ public final class EudiWallet: ObservableObject { /// /// Calls ``storage`` loadDocuments /// - Returns: An array of ``WalletStorage.Document`` objects - @discardableResult public func loadDocuments() async throws -> [WalletStorage.Document]? { - return try await storage.loadDocuments() + @discardableResult public func loadDocuments(status: WalletStorage.DocumentStatus = .issued) async throws -> [WalletStorage.Document]? { + return try await storage.loadDocuments(status: status) } diff --git a/Sources/EudiWalletKit/Services/Enumerations.swift b/Sources/EudiWalletKit/Services/Enumerations.swift index bba0ff9..640431f 100644 --- a/Sources/EudiWalletKit/Services/Enumerations.swift +++ b/Sources/EudiWalletKit/Services/Enumerations.swift @@ -17,6 +17,7 @@ limitations under the License. // TransferStatus.swift import Foundation +import WalletStorage /// Data exchange flow type public enum FlowType: Codable, Hashable { @@ -35,6 +36,16 @@ public enum DataFormat: String { case sdjwt = "sdjwt" } +public extension DataFormat { + init?(_ docDataType: DocDataType) { + switch docDataType { + case .cbor: self = .cbor + case .sjwt: self = .sdjwt + default: return nil + } + } +} + public enum StorageType { case keyChain } diff --git a/Sources/EudiWalletKit/Services/OpenId4VciService.swift b/Sources/EudiWalletKit/Services/OpenId4VciService.swift index 29e0d8a..d937e57 100644 --- a/Sources/EudiWalletKit/Services/OpenId4VciService.swift +++ b/Sources/EudiWalletKit/Services/OpenId4VciService.swift @@ -303,6 +303,13 @@ public class OpenId4VCIService: NSObject, ASWebAuthenticationPresentationContext } } + func requestDeferredIssuance(deferredDoc: WalletStorage.Document) async throws -> IssuanceOutcome { + let model: DeferredIssuanceModel = try JSONDecoder().decode(DeferredIssuanceModel.self, from: deferredDoc.data) + let issuer = try getIssuerForDeferred(data: model) + let authorized: AuthorizedRequest = .noProofRequired(accessToken: model.accessToken, refreshToken: model.refreshToken, credentialIdentifiers: nil) + return try await deferredCredentialUseCase(issuer: issuer, authorized: authorized, transactionId: model.transactionId) + } + private func deferredCredentialUseCase(issuer: Issuer, authorized: AuthorizedRequest, transactionId: TransactionId) async throws -> IssuanceOutcome { logger.info("--> [ISSUANCE] Got a deferred issuance response from server with transaction_id \(transactionId.value). Retrying issuance...") let deferredRequestResponse = try await issuer.requestDeferredIssuance(proofRequest: authorized, transactionId: transactionId) diff --git a/Sources/EudiWalletKit/Services/StorageManager.swift b/Sources/EudiWalletKit/Services/StorageManager.swift index ee7bab8..e2a0d1c 100644 --- a/Sources/EudiWalletKit/Services/StorageManager.swift +++ b/Sources/EudiWalletKit/Services/StorageManager.swift @@ -26,6 +26,7 @@ public class StorageManager: ObservableObject { public static let knownDocTypes = [EuPidModel.euPidDocType, IsoMdlModel.isoDocType] /// Array of document models loaded in the wallet @Published public private(set) var mdocModels: [any MdocDecodable] = [] + @Published public private(set) var deferredDocuments: [WalletStorage.Document] = [] var storageService: any DataStorageService /// Whether wallet currently has loaded data @Published public private(set) var hasData: Bool = false @@ -56,15 +57,26 @@ public class StorageManager: ObservableObject { } @MainActor - fileprivate func refreshDocModels(_ docs: [WalletStorage.Document]) { - mdocModels = docs.compactMap(toModel(doc:)) + fileprivate func refreshDocModels(_ docs: [WalletStorage.Document], docStatus: WalletStorage.DocumentStatus) { + switch docStatus { + case .issued: + mdocModels = docs.compactMap(toModel(doc:)) + case .deferred: + deferredDocuments = docs + } } @MainActor @discardableResult func appendDocModel(_ doc: WalletStorage.Document) -> (any MdocDecodable)? { - let mdoc: (any MdocDecodable)? = toModel(doc: doc) - if let mdoc { mdocModels.append(mdoc) } - return mdoc + switch doc.status { + case .issued: + let mdoc: (any MdocDecodable)? = toModel(doc: doc) + if let mdoc { mdocModels.append(mdoc) } + return mdoc + case .deferred: + deferredDocuments.append(doc) + return nil + } } func toModel(doc: WalletStorage.Document) -> (any MdocDecodable)? { @@ -84,12 +96,12 @@ public class StorageManager: ObservableObject { /// Load documents from storage /// - /// Internally sets the ``docTypes``, ``mdocModels``, ``documentIds``, ``mdocModels``, ``mdlModel``, ``pidModel`` variables + /// Internally sets the ``mdocModels``, ``mdlModel``, ``pidModel`` variables /// - Returns: An array of ``WalletStorage.Document`` objects - @discardableResult public func loadDocuments() async throws -> [WalletStorage.Document]? { + @discardableResult public func loadDocuments(status: WalletStorage.DocumentStatus = .issued) async throws -> [WalletStorage.Document]? { do { - guard let docs = try storageService.loadDocuments(status: .issued) else { return nil } - await refreshDocModels(docs) + guard let docs = try storageService.loadDocuments(status: status) else { return nil } + await refreshDocModels(docs, docStatus: status) await refreshPublishedVars() return docs } catch { @@ -145,17 +157,21 @@ public class StorageManager: ObservableObject { } /// Delete document by Index /// - Parameter index: Index in array of loaded models - func deleteDocument(index: Int) async throws { + func deleteDocument(index: Int, status: DocumentStatus = .issued) async throws { guard index < mdocModels.count else { return } let id = mdocModels[index].id do { - try storageService.deleteDocument(id: id, status: .issued) - await MainActor.run { - if mdocModels[index].docType == IsoMdlModel.isoDocType { mdlModel = nil } - if mdocModels[index].docType == EuPidModel.euPidDocType { pidModel = nil } - mdocModels.remove(at: index) + try storageService.deleteDocument(id: id, status: status) + if status == .issued { + await MainActor.run { + if mdocModels[index].docType == IsoMdlModel.isoDocType { mdlModel = nil } + if mdocModels[index].docType == EuPidModel.euPidDocType { pidModel = nil } + mdocModels.remove(at: index) + } + await refreshPublishedVars() + } else { + await MainActor.run { deferredDocuments.remove(at: index) } } - await refreshPublishedVars() } catch { await setError(error) throw error diff --git a/changelog.md b/changelog.md index 2dc1226..3d4b9b7 100644 --- a/changelog.md +++ b/changelog.md @@ -1,8 +1,12 @@ ## v0.5.7 +### StorageManager changes +- remove `otherModels`, `docTypes`, `documentIds` properties +- `loadDocuments` takes an optional `status` parameter of type `WalletStorage.DocumentStatus` (default is `issued`) +- new variable `@Published public private(set) var deferredDocuments: [WalletStorage.Document] = []` (documents that are not yet issued) +### Other changes - Update eudi-lib-ios-openid4vci-swift to version 0.3.2 - Rename `OfferedIssueModel` to `OfferedIssuanceModel` - `EudiWallet`: added property `public var accessGroup: String?` (used for sharing keychain items between apps with the same access group) -- StorageManager: remove `otherModels`, `docTypes`, `documentIds` properties ## v0.5.6 - Update eudi-lib-ios-siop-openid4vp-swift to version 0.3.2 From 50bc35cd040db79e5064485f07621c3fa8c49899 Mon Sep 17 00:00:00 2001 From: Filippos Sakellaropoulos Date: Tue, 16 Jul 2024 00:27:50 +0300 Subject: [PATCH 08/12] fix for deleteDocument(s) , use createDeferredIssuer for deferred issuer, use vci 0.4.0 --- Package.swift | 2 +- Sources/EudiWalletKit/EudiWallet.swift | 21 ++++++----- .../Services/OpenId4VciService.swift | 22 ++++++------ .../Services/StorageManager.swift | 35 ++++++++----------- .../ViewModels/DeferredIssuanceModel.swift | 1 + 5 files changed, 40 insertions(+), 41 deletions(-) diff --git a/Package.swift b/Package.swift index c32a3ed..b342c94 100644 --- a/Package.swift +++ b/Package.swift @@ -18,7 +18,7 @@ let package = Package( .package(url: "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-iso18013-data-transfer.git", exact: "0.2.9"), .package(url: "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-wallet-storage.git", .upToNextMajor(from: "0.2.0")), .package(url: "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-siop-openid4vp-swift.git", exact: "0.3.2"), - .package(url: "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-openid4vci-swift.git", exact: "0.3.2"), + .package(url: "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-openid4vci-swift.git", exact: "0.4.0"), ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. diff --git a/Sources/EudiWalletKit/EudiWallet.swift b/Sources/EudiWalletKit/EudiWallet.swift index 05cdfc5..026fb7e 100644 --- a/Sources/EudiWalletKit/EudiWallet.swift +++ b/Sources/EudiWalletKit/EudiWallet.swift @@ -110,6 +110,7 @@ public final class EudiWallet: ObservableObject { guard let pkt = deferredDoc.privateKeyType, let pk = deferredDoc.privateKey, let format = DataFormat(deferredDoc.docDataType) else { throw WalletError(description: "Invalid document") } let issueReq = try IssueRequest(id: deferredDoc.id, docType: deferredDoc.docType, privateKeyType: pkt, keyData: pk) let openId4VCIService = OpenId4VCIService(issueRequest: issueReq, credentialIssuerURL: "", clientId: "", callbackScheme: openID4VciRedirectUri, urlSession: urlSession) + openId4VCIService.usedSecureEnclave = deferredDoc.privateKeyType == .secureEnclaveP256 let data = try await openId4VCIService.requestDeferredIssuance(deferredDoc: deferredDoc) return try await finalizeIssuing(id: deferredDoc.id, data: data, docType: deferredDoc.docType, format: format, issueReq: issueReq, openId4VCIService: openId4VCIService) } @@ -139,8 +140,8 @@ public final class EudiWallet: ObservableObject { try endIssueDocument(newDocument) await storage.appendDocModel(newDocument) await storage.refreshPublishedVars() - if !data.isDeferred, let index = storage.deferredDocuments.firstIndex(where: { $0.id == id }) { - try await storage.deleteDocument(index: index) + if !data.isDeferred, storage.deferredDocuments.first(where: { $0.id == id }) != nil { + try await storage.deleteDocument(id: id, status: .deferred) } return newDocument } @@ -213,8 +214,12 @@ public final class EudiWallet: ObservableObject { /// /// Calls ``storage`` loadDocuments /// - Returns: An array of ``WalletStorage.Document`` objects - public func deleteDocuments() async throws { - return try await storage.deleteDocuments() + public func deleteDocuments(status: WalletStorage.DocumentStatus = .issued) async throws { + return try await storage.deleteDocuments(status: status) + } + + public func deleteDocument(id: String, status: WalletStorage.DocumentStatus = .issued) async throws { + return try await storage.deleteDocument(id: id, status: status) } /// Load sample data from json files @@ -227,10 +232,10 @@ public final class EudiWallet: ObservableObject { .compactMap(SignUpResponse.decomposeCBORSignupResponse(data:)).flatMap {$0} .map { Document(docType: $0.docType, docDataType: .cbor, data: $0.issData, privateKeyType: .x963EncodedP256, privateKey: $0.pkData, createdAt: Date.distantPast, modifiedAt: nil, status: .issued) } do { - for docSample in docSamples { - try storageService.saveDocument(docSample, allowOverwrite: true) - } - try await storage.loadDocuments() + for docSample in docSamples { + try storageService.saveDocument(docSample, allowOverwrite: true) + } + try await storage.loadDocuments(status: .issued) } catch { await storage.setError(error) throw WalletError(description: error.localizedDescription, code: (error as NSError).code) diff --git a/Sources/EudiWalletKit/Services/OpenId4VciService.swift b/Sources/EudiWalletKit/Services/OpenId4VciService.swift index d937e57..6e89bef 100644 --- a/Sources/EudiWalletKit/Services/OpenId4VciService.swift +++ b/Sources/EudiWalletKit/Services/OpenId4VciService.swift @@ -95,7 +95,7 @@ public class OpenId4VCIService: NSObject, ASWebAuthenticationPresentationContext } func getIssuerForDeferred(data: DeferredIssuanceModel) throws -> Issuer { - try Issuer(authorizationServerMetadata: .oauth(AuthorizationServerMetadata(issuer: nil, authorizationEndpoint: nil, tokenEndpoint: nil, introspectionEndpoint: nil, jwksURI: nil, grantTypesSupported: nil, responseTypesSupported: nil, requestObjectSigningAlgValuesSupported: nil, requestObjectEncryptionAlgValuesSupported: nil, requestObjectEncryptionEncValuesSupported: nil, responseModesSupported: nil, registrationEndpoint: nil, tokenEndpointAuthMethodsSupported: nil, tokenEndpointAuthSigningAlgValuesSupported: nil, introspectionEndpointAuthMethodsSupported: nil, introspectionEndpointAuthSigningAlgValuesSupported: nil, authorizationSigningAlgValuesSupported: nil, authorizationEncryptionAlgValuesSupported: nil, authorizationEncryptionEncValuesSupported: nil, scopesSupported: nil, requestParameterSupported: nil, requestURIParameterSupported: nil, requireRequestURIRegistration: nil, codeChallengeMethodsSupported: nil, tlsClientCertificateBoundAccessTokens: nil, dpopSigningAlgValuesSupported: nil, revocationEndpoint: nil, revocationEndpointAuthMethodsSupported: nil, revocationEndpointAuthSigningAlgValuesSupported: nil, deviceAuthorizationEndpoint: nil, backchannelTokenDeliveryModesSupported: nil, backchannelAuthenticationEndpoint: nil, backchannelAuthenticationRequestSigningAlgValuesSupported: nil, requirePushedAuthorizationRequests: nil, pushedAuthorizationRequestEndpoint: nil, mtlsEndpointAliases: nil, authorizationResponseIssParameterSupported: nil)), issuerMetadata: CredentialIssuerMetadata(credentialIssuerIdentifier: CredentialIssuerId(""), authorizationServers: [], credentialEndpoint: data.deferredCredentialEndpoint, batchCredentialEndpoint: nil, deferredCredentialEndpoint: data.deferredCredentialEndpoint, notificationEndpoint: nil, credentialConfigurationsSupported: [:], signedMetadata: nil, display: nil), config: config, parPoster: Poster(session: urlSession), tokenPoster: Poster(session: urlSession), requesterPoster: Poster(session: urlSession), deferredRequesterPoster: Poster(session: urlSession), notificationPoster: Poster(session: urlSession)) + try Issuer.createDeferredIssuer(deferredCredentialEndpoint: data.deferredCredentialEndpoint, deferredRequesterPoster: Poster(session: urlSession), config: config) } func issueDocumentsByOfferUrl(offerUri: String, docTypes: [OfferedDocModel], txCodeValue: String?, format: DataFormat, useSecureEnclave: Bool = true, claimSet: ClaimSet? = nil) async throws -> [IssuanceOutcome] { @@ -223,7 +223,7 @@ public class OpenId4VCIService: NSObject, ASWebAuthenticationPresentationContext switch unAuthorized { case .success(let request): let authorizedRequest = await issuer.requestAccessToken(authorizationCode: request) - if case let .success(authorized) = authorizedRequest, case let .noProofRequired(token, _, _) = authorized { + if case let .success(authorized) = authorizedRequest, case let .noProofRequired(token, _, _, _) = authorized { logger.info("--> [AUTHORIZATION] Authorization code exchanged with access token : \(token.accessToken)") return authorized } @@ -238,7 +238,7 @@ public class OpenId4VCIService: NSObject, ASWebAuthenticationPresentationContext private func noProofRequiredSubmissionUseCase(issuer: Issuer, noProofRequiredState: AuthorizedRequest, credentialConfigurationIdentifier: CredentialConfigurationIdentifier, claimSet: ClaimSet? = nil) async throws -> IssuanceOutcome { switch noProofRequiredState { - case .noProofRequired(let accessToken, let refreshToken, _): + case .noProofRequired(let accessToken, let refreshToken, _, let timeStamp): let payload: IssuanceRequestPayload = .configurationBased(credentialConfigurationIdentifier: credentialConfigurationIdentifier, claimSet: claimSet) let responseEncryptionSpecProvider = { Issuer.createResponseEncryptionSpec($0) } let requestOutcome = try await issuer.requestSingle(noProofRequest: noProofRequiredState, requestPayload: payload, responseEncryptionSpecProvider: responseEncryptionSpecProvider) @@ -250,7 +250,7 @@ public class OpenId4VCIService: NSObject, ASWebAuthenticationPresentationContext switch result { case .deferred(let transactionId): //return try await deferredCredentialUseCase(issuer: issuer, authorized: noProofRequiredState, transactionId: transactionId) - let deferredModel = await DeferredIssuanceModel(deferredCredentialEndpoint: issuer.issuerMetadata.deferredCredentialEndpoint!, accessToken: accessToken, refreshToken: refreshToken, transactionId: transactionId) + let deferredModel = await DeferredIssuanceModel(deferredCredentialEndpoint: issuer.issuerMetadata.deferredCredentialEndpoint!, accessToken: accessToken, refreshToken: refreshToken, transactionId: transactionId, timeStamp: timeStamp) return .deferred(deferredModel) case .issued(_, let credential, _): guard let data = Data(base64URLEncoded: credential) else { throw WalletError(description: "Invalid credential") } @@ -272,7 +272,7 @@ public class OpenId4VCIService: NSObject, ASWebAuthenticationPresentationContext } private func proofRequiredSubmissionUseCase(issuer: Issuer, authorized: AuthorizedRequest, credentialConfigurationIdentifier: CredentialConfigurationIdentifier?, claimSet: ClaimSet? = nil) async throws -> IssuanceOutcome { - guard case .proofRequired(let accessToken, let refreshToken, let cNonce, let credentialIdentifiers) = authorized else { throw WalletError(description: "Unexpected AuthorizedRequest case") } + guard case .proofRequired(let accessToken, let refreshToken, let cNonce, let credentialIdentifiers, let timeStamp) = authorized else { throw WalletError(description: "Unexpected AuthorizedRequest case") } guard let credentialConfigurationIdentifier else { throw WalletError(description: "Credential configuration identifier not found") } let payload: IssuanceRequestPayload = .configurationBased(credentialConfigurationIdentifier: credentialConfigurationIdentifier, claimSet: claimSet) let responseEncryptionSpecProvider = { Issuer.createResponseEncryptionSpec($0) } @@ -285,7 +285,7 @@ public class OpenId4VCIService: NSObject, ASWebAuthenticationPresentationContext switch result { case .deferred(let transactionId): //return try await deferredCredentialUseCase(issuer: issuer, authorized: authorized, transactionId: transactionId) - let deferredModel = await DeferredIssuanceModel(deferredCredentialEndpoint: issuer.issuerMetadata.deferredCredentialEndpoint!, accessToken: accessToken, refreshToken: refreshToken, transactionId: transactionId) + let deferredModel = await DeferredIssuanceModel(deferredCredentialEndpoint: issuer.issuerMetadata.deferredCredentialEndpoint!, accessToken: accessToken, refreshToken: refreshToken, transactionId: transactionId, timeStamp: timeStamp) return .deferred(deferredModel) case .issued(_, let credential, _): guard let data = Data(base64URLEncoded: credential) else { throw WalletError(description: "Invalid credential") } @@ -306,7 +306,7 @@ public class OpenId4VCIService: NSObject, ASWebAuthenticationPresentationContext func requestDeferredIssuance(deferredDoc: WalletStorage.Document) async throws -> IssuanceOutcome { let model: DeferredIssuanceModel = try JSONDecoder().decode(DeferredIssuanceModel.self, from: deferredDoc.data) let issuer = try getIssuerForDeferred(data: model) - let authorized: AuthorizedRequest = .noProofRequired(accessToken: model.accessToken, refreshToken: model.refreshToken, credentialIdentifiers: nil) + let authorized: AuthorizedRequest = .noProofRequired(accessToken: model.accessToken, refreshToken: model.refreshToken, credentialIdentifiers: nil, timeStamp: model.timeStamp) return try await deferredCredentialUseCase(issuer: issuer, authorized: authorized, transactionId: model.transactionId) } @@ -322,10 +322,10 @@ public class OpenId4VCIService: NSObject, ASWebAuthenticationPresentationContext case .issuancePending(let transactionId): logger.info("Credential not ready yet. Try after \(transactionId.interval ?? 0)") let deferredModel = switch authorized { - case .noProofRequired(let accessToken, let refreshToken, _): - await DeferredIssuanceModel(deferredCredentialEndpoint: issuer.issuerMetadata.deferredCredentialEndpoint!, accessToken: accessToken, refreshToken: refreshToken, transactionId: transactionId) - case .proofRequired(let accessToken, let refreshToken, _, _): - await DeferredIssuanceModel(deferredCredentialEndpoint: issuer.issuerMetadata.deferredCredentialEndpoint!, accessToken: accessToken, refreshToken: refreshToken, transactionId: transactionId) + case .noProofRequired(let accessToken, let refreshToken, _, let timeStamp): + await DeferredIssuanceModel(deferredCredentialEndpoint: issuer.issuerMetadata.deferredCredentialEndpoint!, accessToken: accessToken, refreshToken: refreshToken, transactionId: transactionId, timeStamp: timeStamp) + case .proofRequired(let accessToken, let refreshToken, _, _, let timeStamp): + await DeferredIssuanceModel(deferredCredentialEndpoint: issuer.issuerMetadata.deferredCredentialEndpoint!, accessToken: accessToken, refreshToken: refreshToken, transactionId: transactionId, timeStamp: timeStamp) } return .deferred(deferredModel) case .errored(_, let errorDescription): diff --git a/Sources/EudiWalletKit/Services/StorageManager.swift b/Sources/EudiWalletKit/Services/StorageManager.swift index e2a0d1c..30d49e9 100644 --- a/Sources/EudiWalletKit/Services/StorageManager.swift +++ b/Sources/EudiWalletKit/Services/StorageManager.swift @@ -98,7 +98,7 @@ public class StorageManager: ObservableObject { /// /// Internally sets the ``mdocModels``, ``mdlModel``, ``pidModel`` variables /// - Returns: An array of ``WalletStorage.Document`` objects - @discardableResult public func loadDocuments(status: WalletStorage.DocumentStatus = .issued) async throws -> [WalletStorage.Document]? { + @discardableResult public func loadDocuments(status: WalletStorage.DocumentStatus) async throws -> [WalletStorage.Document]? { do { guard let docs = try storageService.loadDocuments(status: status) else { return nil } await refreshDocModels(docs, docStatus: status) @@ -146,20 +146,9 @@ public class StorageManager: ObservableObject { /// Delete document by id /// - Parameter id: Document id - public func deleteDocument(id: String) async throws { - guard let i: Array.Index = mdocModels.map(\.id).firstIndex(of: id) else { return } - do { - try await deleteDocument(index: i) - } catch { - await setError(error) - throw error - } - } - /// Delete document by Index - /// - Parameter index: Index in array of loaded models - func deleteDocument(index: Int, status: DocumentStatus = .issued) async throws { - guard index < mdocModels.count else { return } - let id = mdocModels[index].id + public func deleteDocument(id: String, status: DocumentStatus) async throws { + let index = switch status { case .issued: mdocModels.firstIndex(where: { $0.id == id}); default: deferredDocuments.firstIndex(where: { $0.id == id}) } + guard let index else { throw WalletError(description: "Document not found") } do { try storageService.deleteDocument(id: id, status: status) if status == .issued { @@ -169,8 +158,8 @@ public class StorageManager: ObservableObject { mdocModels.remove(at: index) } await refreshPublishedVars() - } else { - await MainActor.run { deferredDocuments.remove(at: index) } + } else if status == .deferred { + await MainActor.run { _ = deferredDocuments.remove(at: index) } } } catch { await setError(error) @@ -179,11 +168,15 @@ public class StorageManager: ObservableObject { } /// Delete documenmts - public func deleteDocuments() async throws { + public func deleteDocuments(status: DocumentStatus) async throws { do { - try storageService.deleteDocuments(status: .issued) - await MainActor.run { mdocModels = []; mdlModel = nil; pidModel = nil } - await refreshPublishedVars() + try storageService.deleteDocuments(status: status) + if status == .issued { + await MainActor.run { mdocModels = []; mdlModel = nil; pidModel = nil } + await refreshPublishedVars() + } else if status == .deferred { + await MainActor.run { deferredDocuments.removeAll(keepingCapacity:false) } + } } catch { await setError(error) throw error diff --git a/Sources/EudiWalletKit/ViewModels/DeferredIssuanceModel.swift b/Sources/EudiWalletKit/ViewModels/DeferredIssuanceModel.swift index bbc9f68..cdbd8a4 100644 --- a/Sources/EudiWalletKit/ViewModels/DeferredIssuanceModel.swift +++ b/Sources/EudiWalletKit/ViewModels/DeferredIssuanceModel.swift @@ -22,6 +22,7 @@ struct DeferredIssuanceModel: Codable { let accessToken: IssuanceAccessToken let refreshToken: IssuanceRefreshToken? let transactionId: TransactionId + let timeStamp: TimeInterval } enum IssuanceOutcome { From 62ed65bb28edcc4fbb390c1b3ef2361377ee297f Mon Sep 17 00:00:00 2001 From: Filippos Sakellaropoulos Date: Tue, 16 Jul 2024 01:02:14 +0300 Subject: [PATCH 09/12] finalize deferred only on issued --- Sources/EudiWalletKit/EudiWallet.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/EudiWalletKit/EudiWallet.swift b/Sources/EudiWalletKit/EudiWallet.swift index 026fb7e..9869884 100644 --- a/Sources/EudiWalletKit/EudiWallet.swift +++ b/Sources/EudiWalletKit/EudiWallet.swift @@ -112,6 +112,7 @@ public final class EudiWallet: ObservableObject { let openId4VCIService = OpenId4VCIService(issueRequest: issueReq, credentialIssuerURL: "", clientId: "", callbackScheme: openID4VciRedirectUri, urlSession: urlSession) openId4VCIService.usedSecureEnclave = deferredDoc.privateKeyType == .secureEnclaveP256 let data = try await openId4VCIService.requestDeferredIssuance(deferredDoc: deferredDoc) + guard case .issued(_) = data else { return deferredDoc } return try await finalizeIssuing(id: deferredDoc.id, data: data, docType: deferredDoc.docType, format: format, issueReq: issueReq, openId4VCIService: openId4VCIService) } From 7fd08f427225fff59be1dc11933bf3d96e022b08 Mon Sep 17 00:00:00 2001 From: Filippos Sakellaropoulos Date: Tue, 16 Jul 2024 01:20:31 +0300 Subject: [PATCH 10/12] update documentation and changelog --- Package.resolved | 4 ++-- Sources/EudiWalletKit/EudiWallet.swift | 16 ++++++---------- changelog.md | 9 +++++++-- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/Package.resolved b/Package.resolved index 476e669..e2a4ff8 100644 --- a/Package.resolved +++ b/Package.resolved @@ -50,8 +50,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-openid4vci-swift.git", "state" : { - "revision" : "9e512f934c9e3a62dbc342381ed94ce194881148", - "version" : "0.3.2" + "revision" : "7a401b73ff0e054828dad87a7f6ff0272462f8c0", + "version" : "0.4.0" } }, { diff --git a/Sources/EudiWalletKit/EudiWallet.swift b/Sources/EudiWalletKit/EudiWallet.swift index 9869884..97f45e0 100644 --- a/Sources/EudiWalletKit/EudiWallet.swift +++ b/Sources/EudiWalletKit/EudiWallet.swift @@ -103,8 +103,12 @@ public final class EudiWallet: ObservableObject { let data = try await openId4VCIService.issueDocument(docType: docType, format: format, useSecureEnclave: useSecureEnclave) return try await finalizeIssuing(id: id, data: data, docType: docType, format: format, issueReq: issueReq, openId4VCIService: openId4VCIService) } - - + + /// Request a deferred issuance based on a stored deferred document. On success, the deferred document is updated with the issued document. + /// + /// The caller does not need to reload documents, storage manager collections are updated. + /// - Parameter deferredDoc: A stored document with deferred status + /// - Returns: The issued document in case it was approved in the backend and the deferred data are valid, otherwise a deferred status document @discardableResult public func requestDeferredIssuance(deferredDoc: WalletStorage.Document) async throws -> WalletStorage.Document { guard deferredDoc.status == .deferred else { throw WalletError(description: "Invalid document status") } guard let pkt = deferredDoc.privateKeyType, let pk = deferredDoc.privateKey, let format = DataFormat(deferredDoc.docDataType) else { throw WalletError(description: "Invalid document") } @@ -181,10 +185,6 @@ public final class EudiWallet: ObservableObject { return documents } - public func re() { - - } - /// Begin issuing a document by generating an issue request /// /// - Parameters: @@ -219,10 +219,6 @@ public final class EudiWallet: ObservableObject { return try await storage.deleteDocuments(status: status) } - public func deleteDocument(id: String, status: WalletStorage.DocumentStatus = .issued) async throws { - return try await storage.deleteDocument(id: id, status: status) - } - /// Load sample data from json files /// /// The mdoc data are stored in wallet storage as documents diff --git a/changelog.md b/changelog.md index 3d4b9b7..7d1011a 100644 --- a/changelog.md +++ b/changelog.md @@ -1,10 +1,15 @@ ## v0.5.7 ### StorageManager changes -- remove `otherModels`, `docTypes`, `documentIds` properties - `loadDocuments` takes an optional `status` parameter of type `WalletStorage.DocumentStatus` (default is `issued`) +- `deleteDocuments` takes an optional `status` parameter of type `WalletStorage.DocumentStatus` (default is `issued`) - new variable `@Published public private(set) var deferredDocuments: [WalletStorage.Document] = []` (documents that are not yet issued) +### Deferred issuance + Request a deferred issuance based on a stored deferred document. On success, the deferred document is updated with the issued document. + The caller does not need to reload documents, storage manager collections are updated. +- `@discardableResult public func requestDeferredIssuance(deferredDoc: WalletStorage.Document) async throws -> WalletStorage.Document` ### Other changes -- Update eudi-lib-ios-openid4vci-swift to version 0.3.2 +- remove `otherModels`, `docTypes`, `documentIds` properties +- Update eudi-lib-ios-openid4vci-swift to version 0.4.0 - Rename `OfferedIssueModel` to `OfferedIssuanceModel` - `EudiWallet`: added property `public var accessGroup: String?` (used for sharing keychain items between apps with the same access group) From 607c1aeb8ae61f29bd99a15d463706fc032f0e0c Mon Sep 17 00:00:00 2001 From: Filippos Sakellaropoulos Date: Tue, 16 Jul 2024 12:35:39 +0300 Subject: [PATCH 11/12] chore: Update eudi-lib-ios-openid4vci-swift to version 0.4.1 --- Package.resolved | 4 ++-- Package.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Package.resolved b/Package.resolved index e2a4ff8..39e52d8 100644 --- a/Package.resolved +++ b/Package.resolved @@ -50,8 +50,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-openid4vci-swift.git", "state" : { - "revision" : "7a401b73ff0e054828dad87a7f6ff0272462f8c0", - "version" : "0.4.0" + "revision" : "b926a5dce8bd372efb496eb48e7a9f90db8d5843", + "version" : "0.4.1" } }, { diff --git a/Package.swift b/Package.swift index b342c94..7b8bb20 100644 --- a/Package.swift +++ b/Package.swift @@ -18,7 +18,7 @@ let package = Package( .package(url: "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-iso18013-data-transfer.git", exact: "0.2.9"), .package(url: "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-wallet-storage.git", .upToNextMajor(from: "0.2.0")), .package(url: "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-siop-openid4vp-swift.git", exact: "0.3.2"), - .package(url: "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-openid4vci-swift.git", exact: "0.4.0"), + .package(url: "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-openid4vci-swift.git", exact: "0.4.1"), ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. From 8227fc51eaf426b7cfbc2b7c8364edcc67e8a92e Mon Sep 17 00:00:00 2001 From: Filippos Sakellaropoulos Date: Tue, 16 Jul 2024 13:34:54 +0300 Subject: [PATCH 12/12] Update Wallet Storage version --- Package.resolved | 4 ++-- Package.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Package.resolved b/Package.resolved index 39e52d8..ee5c05b 100644 --- a/Package.resolved +++ b/Package.resolved @@ -77,8 +77,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-wallet-storage.git", "state" : { - "revision" : "ba9ba8b2b889638d3a4b62358c741b34291848da", - "version" : "0.2.0" + "revision" : "8ebf1f0cf0edb2854b7b098cd90c32e0da5c9c34", + "version" : "0.2.2" } }, { diff --git a/Package.swift b/Package.swift index 7b8bb20..ebc1c69 100644 --- a/Package.swift +++ b/Package.swift @@ -16,7 +16,7 @@ let package = Package( .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), .package(url: "https://github.com/apple/swift-log.git", from: "1.5.3"), .package(url: "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-iso18013-data-transfer.git", exact: "0.2.9"), - .package(url: "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-wallet-storage.git", .upToNextMajor(from: "0.2.0")), + .package(url: "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-wallet-storage.git", exact: "0.2.2"), .package(url: "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-siop-openid4vp-swift.git", exact: "0.3.2"), .package(url: "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-openid4vci-swift.git", exact: "0.4.1"), ],