Skip to content

Commit

Permalink
Merge pull request #106 from niscy-eudiw/fixes
Browse files Browse the repository at this point in the history
Swift 6 migration
  • Loading branch information
phisakel authored Oct 10, 2024
2 parents 511934d + a85cd88 commit a8cb2c5
Show file tree
Hide file tree
Showing 15 changed files with 165 additions and 152 deletions.
11 changes: 2 additions & 9 deletions .github/workflows/swift.yml
Original file line number Diff line number Diff line change
@@ -1,16 +1,9 @@
name: Swift Build

on:
push:
branches-ignore:
- 'dependabot/*'
pull_request_target:
workflow_dispatch:

on: [push]
jobs:
build:
runs-on: macos-latest-xlarge

runs-on: macos-latest
steps:
- uses: maxim-lobanov/setup-xcode@v1
with:
Expand Down
35 changes: 18 additions & 17 deletions Package.resolved
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"originHash" : "52a6b211db7b42edef80ca0a011ade90c5c72e8348f56a62085424df662c0bb9",
"pins" : [
{
"identity" : "blueecc",
Expand All @@ -23,26 +24,26 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-iso18013-data-model.git",
"state" : {
"revision" : "e604f0f0b67c86c3360f848defe85c9a9939b716",
"version" : "0.3.1"
"revision" : "39ba199744ad478544fbad3a73c4a47677f34ec7",
"version" : "0.3.2"
}
},
{
"identity" : "eudi-lib-ios-iso18013-data-transfer",
"kind" : "remoteSourceControl",
"location" : "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-iso18013-data-transfer.git",
"state" : {
"revision" : "0b2741f2ce2b9232e1cf10dce070fcfa3d714dcd",
"version" : "0.3.1"
"revision" : "abc00ef942e9b02e73786726661bd71eb2876b6e",
"version" : "0.3.2"
}
},
{
"identity" : "eudi-lib-ios-iso18013-security",
"kind" : "remoteSourceControl",
"location" : "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-iso18013-security.git",
"state" : {
"revision" : "9d4cc4f403ded786b89401bfbc455ab8f83635db",
"version" : "0.2.4"
"revision" : "89cccb0dec4e675d3d83e9e78076822d98d024bb",
"version" : "0.2.5"
}
},
{
Expand Down Expand Up @@ -77,17 +78,17 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-wallet-storage.git",
"state" : {
"revision" : "373a59f765c80b5319d4064d25c56f75a486af86",
"version" : "0.2.8"
"revision" : "ef829a7eb0d3db82a4dd4bdabc80c930e86d152e",
"version" : "0.2.9"
}
},
{
"identity" : "hitch",
"kind" : "remoteSourceControl",
"location" : "https://github.com/KittyMac/Hitch.git",
"state" : {
"revision" : "77b592f4c21c454a24da2fad80901e6b19751780",
"version" : "0.4.147"
"revision" : "d6c147a1d70992db39a141cb5bf9cf8fbb776250",
"version" : "0.4.148"
}
},
{
Expand Down Expand Up @@ -167,17 +168,17 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-collections.git",
"state" : {
"revision" : "9bf03ff58ce34478e66aaee630e491823326fd06",
"version" : "1.1.3"
"revision" : "671108c96644956dddcd89dd59c203dcdb36cec7",
"version" : "1.1.4"
}
},
{
"identity" : "swift-crypto",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-crypto.git",
"state" : {
"revision" : "9f95b4d033a4edd3814b48608db3f2ca90c7218b",
"version" : "3.7.0"
"revision" : "ffca28be3c9c6a86a579949d23f68818a4b9b5d8",
"version" : "3.8.0"
}
},
{
Expand All @@ -203,8 +204,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/niscy-eudiw/SwiftCBOR.git",
"state" : {
"revision" : "310dbc3975a5653237fed304d88a6dd59d04dd30",
"version" : "0.5.7"
"revision" : "2c8c55273d4c4aae21bb46c2afbae79ee072eff4",
"version" : "0.6.2"
}
},
{
Expand All @@ -226,5 +227,5 @@
}
}
],
"version" : 2
"version" : 3
}
6 changes: 3 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version: 5.9
// swift-tools-version: 6.0
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription
Expand All @@ -15,8 +15,8 @@ let package = Package(
dependencies: [
.package(url: "https://github.com/apple/swift-log.git", from: "1.5.3"),
.package(url: "https://github.com/crspybits/swift-log-file", from: "0.1.0"),
.package(url: "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-iso18013-data-transfer.git", exact: "0.3.1"),
.package(url: "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-wallet-storage.git", exact: "0.2.8"),
.package(url: "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-iso18013-data-transfer.git", exact: "0.3.2"),
.package(url: "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-wallet-storage.git", exact: "0.2.9"),
.package(url: "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-siop-openid4vp-swift.git", exact: "0.4.0"),
.package(url: "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-openid4vci-swift.git", exact: "0.6.0"),
],
Expand Down
97 changes: 55 additions & 42 deletions Sources/EudiWalletKit/EudiWallet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,17 @@ import UIKit
#endif

/// User wallet implementation
@MainActor
public final class EudiWallet: ObservableObject {
/// Storage manager instance
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 = try! EudiWallet()
/// The [service](https://developer.apple.com/documentation/security/ksecattrservice) used to store documents. Use a different service than the default one if you want to store documents in a different location.
public var serviceName: String { didSet { storage.storageService.serviceName = serviceName } }
public var serviceName: String { didSet { Task { try await setServiceParams() } } }
/// The [access group](https://developer.apple.com/documentation/security/ksecattraccessgroup) that documents are stored in.
public var accessGroup: String? { didSet { storage.storageService.accessGroup = accessGroup } }
public var accessGroup: String? { didSet { Task { try await setServiceParams() } } }
/// 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
Expand All @@ -63,17 +64,16 @@ public final class EudiWallet: ObservableObject {
public var urlSession: URLSession
/// If not-nil, logging to the specified log file name will be configured
public var logFileName: String? { didSet { try? initializeLogging() } }
public static var defaultClientId = "wallet-dev"
public static var defaultOpenID4VciRedirectUri = URL(string: "eudi-openid4ci://authorize")!
public static var defaultOpenId4VCIConfig = OpenId4VCIConfig(clientId: defaultClientId, authFlowRedirectionURI: defaultOpenID4VciRedirectUri)

public static var defaultServiceName = "eudiw"
public static let defaultClientId = "wallet-dev"
public static let defaultOpenID4VciRedirectUri = URL(string: "eudi-openid4ci://authorize")!
public static let defaultOpenId4VCIConfig = OpenId4VCIConfig(clientId: defaultClientId, authFlowRedirectionURI: defaultOpenID4VciRedirectUri)
public static let defaultServiceName = "eudiw"
/// Initialize a wallet instance. All parameters are optional.
public init(storageType: StorageType = .keyChain, serviceName: String = defaultServiceName, accessGroup: String? = nil, trustedReaderCertificates: [Data]? = nil, userAuthenticationRequired: Bool = true, verifierApiUri: String? = nil, openID4VciIssuerUrl: String? = nil, openID4VciConfig: OpenId4VCIConfig? = nil, urlSession: URLSession? = nil, logFileName: String? = nil, modelFactory: (any MdocModelFactory.Type)? = nil) throws {
guard !serviceName.isEmpty, !serviceName.contains(":") else { throw WalletError(description: "Not allowed service name, remove : character") }
self.serviceName = serviceName
public init(storageType: StorageType = .keyChain, serviceName: String? = nil, accessGroup: String? = nil, trustedReaderCertificates: [Data]? = nil, userAuthenticationRequired: Bool = true, verifierApiUri: String? = nil, openID4VciIssuerUrl: String? = nil, openID4VciConfig: OpenId4VCIConfig? = nil, urlSession: URLSession? = nil, logFileName: String? = nil, modelFactory: (any MdocModelFactory.Type)? = nil) throws {
try Self.validateServiceParams(serviceName: serviceName)
self.serviceName = serviceName ?? Self.defaultServiceName
self.accessGroup = accessGroup
let keyChainObj = KeyChainStorageService(serviceName: serviceName, accessGroup: accessGroup)
let keyChainObj = KeyChainStorageService(serviceName: self.serviceName, accessGroup: accessGroup)
let storageService = switch storageType { case .keyChain:keyChainObj }
storage = StorageManager(storageService: storageService, modelFactory: modelFactory)
self.trustedReaderCertificates = trustedReaderCertificates
Expand All @@ -94,10 +94,26 @@ public final class EudiWallet: ObservableObject {
/// The file is created in the caches directory
/// - Parameter fileName: A file name
/// - Returns: Th URL of a log file stored in the caches directory
public static func getLogFileURL(_ fileName: String) throws -> URL? {
nonisolated public static func getLogFileURL(_ fileName: String) throws -> URL? {
return try FileManager.getCachesDirectory().appendingPathComponent(fileName)
}

private static func validateServiceParams(serviceName: String? = nil) throws {
guard (serviceName?.contains(":") ?? false) == false else {
let msg = "Not allowed service name, contains : character"
logger.error("validateServiceParams:\(msg)")
throw WalletError(description: msg)
}
}

private func setServiceParams() async throws {
if let keyChainObj = storage.storageService as? KeyChainStorageService {
try Self.validateServiceParams(serviceName: self.serviceName)
await keyChainObj.initialize(serviceName, accessGroup)
}
}


/// Get the contents of a log file stored in the caches directory
/// - Parameter fileName: A file name
/// - Returns: The file contents
Expand All @@ -116,20 +132,20 @@ public final class EudiWallet: ObservableObject {
}

private func initializeLogging() throws {
LoggingSystem.bootstrap { [unowned self] label in
LoggingSystem.bootstrap { [logFileName] label in
var handlers:[LogHandler] = []
if _isDebugAssertConfiguration() {
handlers.append(StreamLogHandler.standardOutput(label: label))
}
#if canImport(UIKit)
if let logFileName {
do {
let logFileURL = try Self.getLogFileURL(logFileName)
guard let logFileURL else { throw WalletError(description: "Cannot create URL for file name \(logFileName)") }
let fileLogger = try FileLogging(to: logFileURL)
handlers.append(FileLogHandler(label: label, fileLogger: fileLogger))
} catch { fatalError("Logging setup failed: \(error.localizedDescription)") }
}
if let logFileName {
do {
let logFileURL = try Self.getLogFileURL(logFileName)
guard let logFileURL else { throw WalletError(description: "Cannot create URL for file name \(logFileName)") }
let fileLogger = try FileLogging(to: logFileURL)
handlers.append(FileLogHandler(label: label, fileLogger: fileLogger))
} catch { fatalError("Logging setup failed: \(error.localizedDescription)") }
}
#endif
return MultiplexLogHandler(handlers)
}
Expand Down Expand Up @@ -189,7 +205,6 @@ public final class EudiWallet: ObservableObject {
/// - Parameter pendingDoc: A temporary document with pending status
///
/// - Returns: The issued document in case it was approved in the backend and the pendingDoc data are valid, otherwise a pendingDoc status document
@MainActor
@discardableResult public func resumePendingIssuance(pendingDoc: WalletStorage.Document, webUrl: URL?) async throws -> WalletStorage.Document {
guard pendingDoc.status == .pending else { throw WalletError(description: "Invalid document status") }
guard let pkt = pendingDoc.privateKeyType, let pk = pendingDoc.privateKey, let format = DataFormat(pendingDoc.docDataType) else { throw WalletError(description: "Invalid document") }
Expand Down Expand Up @@ -226,11 +241,11 @@ public final class EudiWallet: ObservableObject {
}
let newDocStatus: WalletStorage.DocumentStatus = data.isDeferred ? .deferred : (data.isPending ? .pending : .issued)
let newDocument = WalletStorage.Document(id: id, docType: docTypeToSave, docDataType: ddt, data: dataToSave, privateKeyType: (openId4VCIService.usedSecureEnclave ?? true) ? .secureEnclaveP256 : .x963EncodedP256, privateKey: issueReq.keyData, createdAt: Date(), displayName: displayName, status: newDocStatus)
if newDocStatus == .pending { await storage.appendDocModel(newDocument); return newDocument }
try issueReq.saveToStorage(storage.storageService, status: newDocStatus)
try endIssueDocument(newDocument)
await storage.appendDocModel(newDocument)
await storage.refreshPublishedVars()
if newDocStatus == .pending { storage.appendDocModel(newDocument); return newDocument }
try await issueReq.saveTo(storageService: storage.storageService, status: newDocStatus)
try await endIssueDocument(newDocument)
storage.appendDocModel(newDocument)
storage.refreshPublishedVars()
if pds == nil { try await storage.removePendingOrDeferredDoc(id: id) }
return newDocument
}
Expand Down Expand Up @@ -276,14 +291,14 @@ public final class EudiWallet: ObservableObject {
/// - issuer: Issuer function
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, status: bDeferred ? .deferred : .issued) }
if saveToStorage { try await request.saveTo(storageService: 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.saveDocument(issued, allowOverwrite: true)
public func endIssueDocument(_ issued: WalletStorage.Document) async throws {
try await storage.storageService.saveDocument(issued, allowOverwrite: true)
}

/// Load documents with a specific status from storage
Expand Down Expand Up @@ -351,17 +366,17 @@ 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(status: .issued)
try? await 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, displayName: $0.docType == EuPidModel.euPidDocType ? "PID" : ($0.docType == IsoMdlModel.isoDocType ? "mDL" : $0.docType), status: .issued) }
do {
for docSample in docSamples {
try storageService.saveDocument(docSample, allowOverwrite: true)
try await storageService.saveDocument(docSample, allowOverwrite: true)
}
try await storage.loadDocuments(status: .issued)
} catch {
await storage.setError(error)
storage.setError(error)
throw WalletError(description: error.localizedDescription)
}
}
Expand All @@ -371,11 +386,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] {
public func prepareServiceDataParameters(docType: String? = nil, dataFormat: DataFormat = .cbor ) async throws -> [String: Any] {
var parameters: [String: Any]
switch dataFormat {
case .cbor:
guard var docs = try storageService.loadDocuments(status: .issued), docs.count > 0 else { throw WalletError(description: "No documents found") }
guard var docs = try await 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() }
Expand All @@ -395,9 +410,9 @@ public final class EudiWallet: ObservableObject {
/// - docType: DocType of documents to present (optional)
/// - dataFormat: Exchanged data ``Format`` type
/// - Returns: A presentation session instance,
public func beginPresentation(flow: FlowType, docType: String? = nil, dataFormat: DataFormat = .cbor) -> PresentationSession {
public func beginPresentation(flow: FlowType, docType: String? = nil, dataFormat: DataFormat = .cbor) async -> PresentationSession {
do {
let parameters = try prepareServiceDataParameters(docType: docType, dataFormat: dataFormat)
let parameters = try await prepareServiceDataParameters(docType: docType, dataFormat: dataFormat)
let docIdAndTypes = storage.getDocIdsToTypes()
switch flow {
case .ble:
Expand Down Expand Up @@ -425,17 +440,15 @@ public final class EudiWallet: ObservableObject {
PresentationSession(presentationService: service, docIdAndTypes: storage.getDocIdsToTypes(), userAuthenticationRequired: userAuthenticationRequired)
}

@MainActor
/// Perform an action after user authorization via TouchID/FaceID/Passcode
/// - Parameters:
/// - dismiss: Action to perform if the user cancels authorization
/// - action: Action to perform after user authorization
public static func authorizedAction<T>(action: () async throws -> T, disabled: Bool, dismiss: () -> Void, localizedReason: String) async throws -> T? {
public static func authorizedAction<T: Sendable>(action: () async throws -> T, disabled: Bool, dismiss: () -> Void, localizedReason: String) async throws -> T? {
return try await authorizedAction(isFallBack: false, action: action, disabled: disabled, dismiss: dismiss, localizedReason: localizedReason)
}

@MainActor
/// Executes an authorized action with optional fallback and dismissal handling.
/// Executes an authorized action with optional fallback and dismissal handling.
/// The action is performed after successful biometric authentication (TouchID or FaceID).
///
/// - Parameters:
Expand All @@ -449,7 +462,7 @@ public final class EudiWallet: ObservableObject {
/// - Returns: An optional result of type `T` if the action is successful, otherwise `nil`.
///
/// - Throws: An error if the action fails.
static func authorizedAction<T>(isFallBack: Bool = false, action: () async throws -> T, disabled: Bool, dismiss: () -> Void, localizedReason: String) async throws -> T? {
static func authorizedAction<T: Sendable>(isFallBack: Bool = false, action: () async throws -> T, disabled: Bool, dismiss: () -> Void, localizedReason: String) async throws -> T? {
guard !disabled else {
return try await action()
}
Expand Down
Loading

0 comments on commit a8cb2c5

Please sign in to comment.