diff --git a/DfnsDemo/DfnsDemo.xcodeproj/project.pbxproj b/DfnsDemo/DfnsDemo.xcodeproj/project.pbxproj index dc8f1cc..3e33b68 100644 --- a/DfnsDemo/DfnsDemo.xcodeproj/project.pbxproj +++ b/DfnsDemo/DfnsDemo.xcodeproj/project.pbxproj @@ -3,17 +3,17 @@ archiveVersion = 1; classes = { }; - objectVersion = 56; + objectVersion = 60; objects = { /* Begin PBXBuildFile section */ + 48F0B3B02ED061F300500085 /* DfnsSdk in Frameworks */ = {isa = PBXBuildFile; productRef = 48F0B3AF2ED061F300500085 /* DfnsSdk */; }; 7A1A68702BBEA54B00167ED0 /* DfnsDemoApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1A686F2BBEA54B00167ED0 /* DfnsDemoApp.swift */; }; 7A1A68722BBEA54B00167ED0 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1A68712BBEA54B00167ED0 /* ContentView.swift */; }; 7A1A68742BBEA54C00167ED0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7A1A68732BBEA54C00167ED0 /* Assets.xcassets */; }; 7A1A68772BBEA54C00167ED0 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7A1A68762BBEA54C00167ED0 /* Preview Assets.xcassets */; }; 7A1A68A92BC5943700167ED0 /* MyServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1A68A82BC5943700167ED0 /* MyServer.swift */; }; 7A439BB12BCD22710022D861 /* MyBusinessLogic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A439BB02BCD22710022D861 /* MyBusinessLogic.swift */; }; - 7A439BB72BCD67270022D861 /* DfnsSdk in Frameworks */ = {isa = PBXBuildFile; productRef = 7A439BB62BCD67270022D861 /* DfnsSdk */; }; 7A5207E72BCD8B22003EC637 /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5207E62BCD8B22003EC637 /* Config.swift */; }; /* End PBXBuildFile section */ @@ -34,7 +34,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 7A439BB72BCD67270022D861 /* DfnsSdk in Frameworks */, + 48F0B3B02ED061F300500085 /* DfnsSdk in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -97,7 +97,7 @@ ); name = DfnsDemo; packageProductDependencies = ( - 7A439BB62BCD67270022D861 /* DfnsSdk */, + 48F0B3AF2ED061F300500085 /* DfnsSdk */, ); productName = DfnsDemo; productReference = 7A1A686C2BBEA54B00167ED0 /* DfnsDemo.app */; @@ -128,7 +128,7 @@ ); mainGroup = 7A1A68632BBEA54B00167ED0; packageReferences = ( - 7A439BB52BCD67270022D861 /* XCRemoteSwiftPackageReference "dfns-sdk-swift" */, + 48F0B3AE2ED061F300500085 /* XCLocalSwiftPackageReference "../../dfns-sdk-swift" */, ); productRefGroup = 7A1A686D2BBEA54B00167ED0 /* Products */; projectDirPath = ""; @@ -227,6 +227,8 @@ SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_STRICT_CONCURRENCY = complete; + SWIFT_VERSION = 6.0; }; name = Debug; }; @@ -282,6 +284,8 @@ MTL_FAST_MATH = YES; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_STRICT_CONCURRENCY = complete; + SWIFT_VERSION = 6.0; VALIDATE_PRODUCT = YES; }; name = Release; @@ -313,7 +317,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; + SWIFT_STRICT_CONCURRENCY = minimal; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -345,7 +349,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; + SWIFT_STRICT_CONCURRENCY = minimal; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; @@ -373,21 +377,16 @@ }; /* End XCConfigurationList section */ -/* Begin XCRemoteSwiftPackageReference section */ - 7A439BB52BCD67270022D861 /* XCRemoteSwiftPackageReference "dfns-sdk-swift" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/dfns/dfns-sdk-swift"; - requirement = { - branch = m; - kind = branch; - }; +/* Begin XCLocalSwiftPackageReference section */ + 48F0B3AE2ED061F300500085 /* XCLocalSwiftPackageReference "../../dfns-sdk-swift" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = "../../dfns-sdk-swift"; }; -/* End XCRemoteSwiftPackageReference section */ +/* End XCLocalSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 7A439BB62BCD67270022D861 /* DfnsSdk */ = { + 48F0B3AF2ED061F300500085 /* DfnsSdk */ = { isa = XCSwiftPackageProductDependency; - package = 7A439BB52BCD67270022D861 /* XCRemoteSwiftPackageReference "dfns-sdk-swift" */; productName = DfnsSdk; }; /* End XCSwiftPackageProductDependency section */ diff --git a/DfnsDemo/DfnsDemo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DfnsDemo/DfnsDemo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index 163bf7e..0000000 --- a/DfnsDemo/DfnsDemo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,15 +0,0 @@ -{ - "originHash" : "4a21ff1bb0c8364c763bf1da609a1d60bf5c031cbce0665704b506df6d3af9a7", - "pins" : [ - { - "identity" : "dfns-sdk-swift", - "kind" : "remoteSourceControl", - "location" : "https://github.com/dfns/dfns-sdk-swift", - "state" : { - "branch" : "m", - "revision" : "96b0ad2d93d967aa73893ea19f9b7f5b8eccaa98" - } - } - ], - "version" : 3 -} diff --git a/DfnsDemo/DfnsDemo/Config.swift b/DfnsDemo/DfnsDemo/Config.swift index 0b2537c..1178b5e 100644 --- a/DfnsDemo/DfnsDemo/Config.swift +++ b/DfnsDemo/DfnsDemo/Config.swift @@ -1,3 +1,4 @@ +// The url below should match the value in .entitlements file enum Config { public static let serverUrl: String = "https://airedale-finer-baboon.ngrok-free.app" public static let passkeyRelyingPartyId: String = "airedale-finer-baboon.ngrok-free.app" diff --git a/DfnsDemo/DfnsDemo/ContentView.swift b/DfnsDemo/DfnsDemo/ContentView.swift index 62f3f64..04edf19 100644 --- a/DfnsDemo/DfnsDemo/ContentView.swift +++ b/DfnsDemo/DfnsDemo/ContentView.swift @@ -1,8 +1,8 @@ import SwiftUI struct ContentView: View { - @ObservedObject var userConfig: UserConfig - @ObservedObject var myBusinessLogic: MyBusinessLogic + @Binding var userConfig: UserConfig + let myBusinessLogic: MyBusinessLogic var body: some View { NavigationView { @@ -31,7 +31,7 @@ struct ContentView: View { Text("Your customers, either new or existing, must register with Dfns first and have credential(s) in our system in order to own and be able to interact with their blockchain wallets.\n\nThe delegated registration flow allows you to initiate and and complete the registration process on your customers behalf, without them being aware that the wallets infrastructure is powered by Dfns, i.e. they will not receive an registration email from Dfns directly unlike the normal registration process for your employees. Their WebAuthn credentials are still completely under their control.") - NavigationLink("Go to Delegated Registration", destination: DelegatedRegistrationView(userConfig: userConfig, myBusinessLogic: myBusinessLogic)).buttonStyle(.borderedProminent).padding(.vertical, 15) + NavigationLink("Go to Delegated Registration", destination: DelegatedRegistrationView(userConfig: $userConfig, myBusinessLogic: myBusinessLogic)).buttonStyle(.borderedProminent).padding(.vertical, 15) /// STEP 2 @@ -42,7 +42,7 @@ struct ContentView: View { Text("The delegated signing flow does not need the end user sign with the WebAuthn credential. The login can be performed on the server side transparent to the end users and obtain a readonly auth token. For example, your server can choose to automatically login the end users upon the completion of delegated registration. In this tutorial, this step is shown as explicit in order to more clearly demonstrate how the interaction works.") - NavigationLink("Go to Delegated Login", destination: DelegatedLoginView(userConfig: userConfig, myBusinessLogic: myBusinessLogic)).buttonStyle(.borderedProminent).padding(.vertical, 15) + NavigationLink("Go to Delegated Login", destination: DelegatedLoginView(userConfig: $userConfig, myBusinessLogic: myBusinessLogic)).buttonStyle(.borderedProminent).padding(.vertical, 15) /// STEP 3 @@ -69,8 +69,8 @@ struct ContentView: View { } struct DelegatedRegistrationView: View { - @ObservedObject var userConfig: UserConfig - @ObservedObject var myBusinessLogic: MyBusinessLogic + @Binding var userConfig: UserConfig + let myBusinessLogic: MyBusinessLogic @State var registerResponse: String = "" var body: some View { @@ -90,13 +90,20 @@ struct DelegatedRegistrationView: View { .frame(maxWidth: .infinity, alignment: .leading) TextField("Choose a username", text: $userConfig.email).textFieldStyle(.roundedBorder).padding(.vertical) + .textFieldStyle(.roundedBorder) + .padding(.vertical) + .keyboardType(.emailAddress) + .textInputAutocapitalization(.never) Button("Register EndUser") { Task { let result = await myBusinessLogic.registerUser(userConfig: userConfig) registerResponse = result } - }.buttonStyle(.borderedProminent).frame(maxWidth: .infinity).padding(.bottom) + } + .buttonStyle(.borderedProminent) + .frame(maxWidth: .infinity) + .padding(.bottom) JSONText(registerResponse) }.padding() @@ -106,8 +113,8 @@ struct DelegatedRegistrationView: View { } struct DelegatedLoginView: View { - @ObservedObject var userConfig: UserConfig - @ObservedObject var myBusinessLogic: MyBusinessLogic + @Binding var userConfig: UserConfig + let myBusinessLogic: MyBusinessLogic @State var loginResponse: String = "" var body: some View { @@ -123,7 +130,10 @@ struct DelegatedLoginView: View { Text("This auth token is readonly and needs to be cached and passed along with all requests interacting with the Dfns API. To clearly demonstrate all the necessary components for each step, this example will cache the auth token in the application context and send it back with every sequently request to the server. You should however choose a more secure caching method.").padding(.vertical) - TextField("Enter the username", text: $userConfig.email).textFieldStyle(.roundedBorder) + TextField("Enter the username", text: $userConfig.email) + .textFieldStyle(.roundedBorder) + .keyboardType(.emailAddress) + .textInputAutocapitalization(.never) Button("Login EndUser") { Task { @@ -142,8 +152,8 @@ struct DelegatedLoginView: View { } struct EndUserWalletsView: View { - @ObservedObject var userConfig: UserConfig - @ObservedObject var myBusinessLogic: MyBusinessLogic + let userConfig: UserConfig + let myBusinessLogic: MyBusinessLogic @State var walletResponse: String = "" @State var messageToSign: String = "" @State var signingResponse: String = "" diff --git a/DfnsDemo/DfnsDemo/DfnsDemoApp.swift b/DfnsDemo/DfnsDemo/DfnsDemoApp.swift index 81baa45..cb54da6 100644 --- a/DfnsDemo/DfnsDemo/DfnsDemoApp.swift +++ b/DfnsDemo/DfnsDemo/DfnsDemoApp.swift @@ -1,25 +1,29 @@ import DfnsSdk import SwiftUI +import Observation -class UserConfig: ObservableObject { +@Observable +final class UserConfig { + var authToken: String? + var email: String + init() { - email = "" + email = "" } - @Published var authToken: String? - @Published var email: String + } @main struct DfnsDemoApp: App { - @StateObject private var userConfig = UserConfig() - @StateObject private var myBusinessLogic = MyBusinessLogic( + @State private var userConfig = UserConfig() + @State private var myBusinessLogic = MyBusinessLogic( url: Config.serverUrl, passkeyRelyingPartyId: Config.passkeyRelyingPartyId ) var body: some Scene { WindowGroup { - ContentView(userConfig: userConfig, myBusinessLogic: myBusinessLogic) + ContentView(userConfig: $userConfig, myBusinessLogic: myBusinessLogic) } } } diff --git a/DfnsDemo/DfnsDemo/MyBusinessLogic.swift b/DfnsDemo/DfnsDemo/MyBusinessLogic.swift index 5f6fa05..304979e 100644 --- a/DfnsDemo/DfnsDemo/MyBusinessLogic.swift +++ b/DfnsDemo/DfnsDemo/MyBusinessLogic.swift @@ -1,13 +1,16 @@ import DfnsSdk import Foundation +import Observation /** Controller that is doing the interface between the UI, the Demo Server and the Passkey Signer */ -final class MyBusinessLogic: ObservableObject { +@Observable +@MainActor +final class MyBusinessLogic: @unchecked Sendable { private var passkeyRelyingPartyId: String - private var myServer: MyServer - private var passkeysSigner: PasskeysSigner + private let myServer: MyServer + private let passkeysSigner: PasskeysSigner init(url: String, passkeyRelyingPartyId: String) { self.passkeyRelyingPartyId = passkeyRelyingPartyId diff --git a/DfnsDemo/DfnsDemo/MyServer.swift b/DfnsDemo/DfnsDemo/MyServer.swift index 6efdc8d..190ccc3 100644 --- a/DfnsDemo/DfnsDemo/MyServer.swift +++ b/DfnsDemo/DfnsDemo/MyServer.swift @@ -4,8 +4,9 @@ import Foundation /** Implement the API of the server */ -final class MyServer { - private var url: String = "" +@MainActor +final class MyServer: @unchecked Sendable { + private let url: String init(url: String) { self.url = url diff --git a/DfnsDemo/Package.swift b/DfnsDemo/Package.swift new file mode 100644 index 0000000..88cb976 --- /dev/null +++ b/DfnsDemo/Package.swift @@ -0,0 +1,13 @@ +// swift-tools-version:6.2 + +// This is a HACKY workaround the fact that SPM does not allow for Package level exclusion +// of files/folders. SPM actually HAD support for it but it was removed in 2017, in PR +// https://github.com/apple/swift-package-manager/commit/cb69accf41da55386f9703308958aa49ca2a4c5f +// +// So instead we have to add an empty dummy Package.swift to each folder we wanna hide, as per: +// See: https://github.com/apple/swift-package-manager/issues/4460#issuecomment-1475025748 +// And: https://stackoverflow.com/questions/69382302/swift-package-how-to-exclude-files-in-root-git-directory-from-the-actual-swift/70990534#70990534 +// And: https://github.com/tuist/tuist/pull/2058 +import PackageDescription + +let package = Package(name: "HIDDEN") diff --git a/Package.swift b/Package.swift index 545668b..5f8e8ee 100644 --- a/Package.swift +++ b/Package.swift @@ -1,10 +1,16 @@ -// swift-tools-version: 5.10 +// swift-tools-version: 6.2 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( name: "DfnsSdk", + platforms: [ + .macOS(.v10_15), + .iOS(.v15), + .watchOS(.v6), + .tvOS(.v13) + ], products: [ // Products define the executables and libraries a package produces, making them visible to other packages. .library( diff --git a/Sources/DfnsSdk/DfnsApi.swift b/Sources/DfnsSdk/DfnsApi.swift index 092a5b1..22da994 100644 --- a/Sources/DfnsSdk/DfnsApi.swift +++ b/Sources/DfnsSdk/DfnsApi.swift @@ -1,9 +1,28 @@ /** - Types defined in the Dfns API that might be arguments or return values of the demo server - */ -public enum DfnsApi { - public struct UserActionChallenge: Codable { - public init(attestation: String, userVerification: String, externalAuthenticationUrl: String, challenge: String, challengeIdentifier: String, supportedCredentialKinds: [DfnsApi.SupportedCredentialKind], allowCredentials: DfnsApi.AllowCredentials) { + Types defined in the Dfns API that might be arguments or return values of the demo server +*/ +public enum DfnsApi {} // just a namespace + +// MARK: - UserActionChallenge +extension DfnsApi { + public struct UserActionChallenge: Sendable, Codable { + public let attestation: String + public let userVerification: String + public let externalAuthenticationUrl: String + public let challenge: String + public let challengeIdentifier: String + public let supportedCredentialKinds: [SupportedCredentialKind] + public let allowCredentials: AllowCredentials + + public init( + attestation: String, + userVerification: String, + externalAuthenticationUrl: String, + challenge: String, + challengeIdentifier: String, + supportedCredentialKinds: [SupportedCredentialKind], + allowCredentials: AllowCredentials + ) { self.attestation = attestation self.userVerification = userVerification self.externalAuthenticationUrl = externalAuthenticationUrl @@ -12,18 +31,33 @@ public enum DfnsApi { self.supportedCredentialKinds = supportedCredentialKinds self.allowCredentials = allowCredentials } + } +} - public let attestation: String - public let userVerification: String - public let externalAuthenticationUrl: String - public let challenge: String - public let challengeIdentifier: String - public let supportedCredentialKinds: [SupportedCredentialKind] - public let allowCredentials: AllowCredentials - } +// MARK: - UserRegistrationChallenge +extension DfnsApi { + public struct UserRegistrationChallenge: Sendable, Codable { + public let temporaryAuthenticationToken: String + public let user: UserInformation + public let supportedCredentialKinds: SupportedCredentialKinds + public let otpUrl: String + public let challenge: String + public let authenticatorSelection: AuthenticatorSelectionCriteria + public let attestation: String + public let pubKeyCredParams: [PublicKeyCredentialParameters] + public let excludeCredentials: [PublicKeyCredentialDescriptor] - public struct UserRegistrationChallenge: Codable { - public init(temporaryAuthenticationToken: String, user: DfnsApi.UserInformation, supportedCredentialKinds: DfnsApi.SupportedCredentialKinds, otpUrl: String, challenge: String, authenticatorSelection: DfnsApi.AuthenticatorSelectionCriteria, attestation: String, pubKeyCredParams: [DfnsApi.PublicKeyCredentialParameters], excludeCredentials: [DfnsApi.PublicKeyCredentialDescriptor]) { + public init( + temporaryAuthenticationToken: String, + user: UserInformation, + supportedCredentialKinds: SupportedCredentialKinds, + otpUrl: String, + challenge: String, + authenticatorSelection: AuthenticatorSelectionCriteria, + attestation: String, + pubKeyCredParams: [PublicKeyCredentialParameters], + excludeCredentials: [PublicKeyCredentialDescriptor] + ) { self.temporaryAuthenticationToken = temporaryAuthenticationToken self.user = user self.supportedCredentialKinds = supportedCredentialKinds @@ -34,174 +68,257 @@ public enum DfnsApi { self.pubKeyCredParams = pubKeyCredParams self.excludeCredentials = excludeCredentials } + } +} - public let temporaryAuthenticationToken: String - public let user: UserInformation - public let supportedCredentialKinds: SupportedCredentialKinds - public let otpUrl: String - public let challenge: String - public let authenticatorSelection: AuthenticatorSelectionCriteria - public let attestation: String - public let pubKeyCredParams: [PublicKeyCredentialParameters] - public let excludeCredentials: [PublicKeyCredentialDescriptor] - } +// MARK: - RelyingParty +extension DfnsApi { + public struct RelyingParty: Sendable, Codable { + public let id: String + public let name: String - public struct RelyingParty: Codable { - public init(id: String, name: String) { + public init( + id: String, + name: String + ) { self.id = id self.name = name } + } +} - public let id: String - public let name: String - } - public struct SupportedCredentialKind: Codable { - public init(kind: String, factor: String, requiresSecondFactor: Bool) { +// MARK: - SupportedCredentialKind +extension DfnsApi { + public struct SupportedCredentialKind: Sendable, Codable { + public let kind: String + public let factor: String + public let requiresSecondFactor: Bool + + public init( + kind: String, + factor: String, + requiresSecondFactor: Bool + ) { self.kind = kind self.factor = factor self.requiresSecondFactor = requiresSecondFactor } + } +} - public let kind: String - public let factor: String - public let requiresSecondFactor: Bool - } +// MARK: - AllowCredentials +extension DfnsApi { + public struct AllowCredentials: Sendable, Codable { + public let webauthn: [PublicKeyCredentialDescriptor] + public let key: [PublicKeyCredentialDescriptor] - public struct AllowCredentials: Codable { - public init(webauthn: [DfnsApi.PublicKeyCredentialDescriptor], key: [DfnsApi.PublicKeyCredentialDescriptor]) { + public init( + webauthn: [PublicKeyCredentialDescriptor], + key: [PublicKeyCredentialDescriptor] + ) { self.webauthn = webauthn self.key = key } + } +} - public let webauthn: [PublicKeyCredentialDescriptor] - public let key: [PublicKeyCredentialDescriptor] - } +// MARK: - PublicKeyCredentialDescriptor +extension DfnsApi { + public struct PublicKeyCredentialDescriptor: Sendable, Codable { + public let type: String + public let id: String - public struct PublicKeyCredentialDescriptor: Codable { - public init(type: String, id: String) { + public init( + type: String, + id: String + ) { self.type = type self.id = id } + } +} - public let type: String - public let id: String - } +// MARK: - Fido2Assertion +extension DfnsApi { + public struct Fido2Assertion: Sendable, Codable { + public let kind: String + public let credentialAssertion: Fido2AssertionData - public struct Fido2Assertion: Codable { - public init(kind: String, credentialAssertion: DfnsApi.Fido2AssertionData) { + public init( + kind: String, + credentialAssertion: Fido2AssertionData + ) { self.kind = kind self.credentialAssertion = credentialAssertion } + } +} - public let kind: String - public let credentialAssertion: Fido2AssertionData - } +// MARK: - UserActionAssertion +extension DfnsApi { + public struct UserActionAssertion: Sendable, Codable { + public let challengeIdentifier: String + public let firstFactor: Fido2Assertion - public struct UserActionAssertion: Codable { - public init(challengeIdentifier: String, firstFactor: DfnsApi.Fido2Assertion) { + public init( + challengeIdentifier: String, + firstFactor: Fido2Assertion + ) { self.challengeIdentifier = challengeIdentifier self.firstFactor = firstFactor } + } +} - public let challengeIdentifier: String - public let firstFactor: Fido2Assertion - } +// MARK: - ClientData +extension DfnsApi { + public struct ClientData: Sendable, Codable { + public let type: String + public let challenge: String + public let origin: String - public struct ClientData: Codable { - public init(type: String, challenge: String, origin: String) { + public init( + type: String, + challenge: String, + origin: String + ) { self.type = type self.challenge = challenge self.origin = origin } - - public let type: String - public let challenge: String - public let origin: String - // public let crossOrigin: Bool } +} - public struct Fido2AssertionData: Codable { - public init(clientData: String, credId: String, signature: String, authenticatorData: String, userHandle: String? = nil) { - self.clientData = clientData - self.credId = credId - self.signature = signature - self.authenticatorData = authenticatorData - self.userHandle = userHandle - } +// MARK: - Fido2AssertionData +extension DfnsApi { + public struct Fido2AssertionData: Sendable, Codable { + public let clientData: String + public let credId: String + public let signature: String + public var authenticatorData: String + public var userHandle: String? + + public init( + clientData: String, + credId: String, + signature: String, + authenticatorData: String, + userHandle: String? = nil + ) { + self.clientData = clientData + self.credId = credId + self.signature = signature + self.authenticatorData = authenticatorData + self.userHandle = userHandle + } + } +} - public let clientData: String - public let credId: String - public let signature: String - public var authenticatorData: String - public var userHandle: String? - } +// MARK: - PublicKeyCredentialParameters +extension DfnsApi { + public struct PublicKeyCredentialParameters: Sendable, Codable { + public let type: String + public let alg: Int - public struct PublicKeyCredentialParameters: Codable { - public init(type: String, alg: Int) { + public init( + type: String, + alg: Int + ) { self.type = type self.alg = alg } + } +} - public let type: String - public let alg: Int - } +// MARK: - SupportedCredentialKinds +extension DfnsApi { + public struct SupportedCredentialKinds: Sendable, Codable { + public let firstFactor: [String] + public let secondFactor: [String] - public struct SupportedCredentialKinds: Codable { - public init(firstFactor: [String], secondFactor: [String]) { + public init( + firstFactor: [String], + secondFactor: [String] + ) { self.firstFactor = firstFactor self.secondFactor = secondFactor } + } +} - public let firstFactor: [String] - public let secondFactor: [String] - } +// MARK: - UserInformation +extension DfnsApi { + public struct UserInformation: Sendable, Codable { + public let id: String + public let displayName: String + public let name: String - public struct UserInformation: Codable { - public init(id: String, displayName: String, name: String) { + public init( + id: String, + displayName: String, + name: String + ) { self.id = id self.displayName = displayName self.name = name } + } +} - public let id: String - public let displayName: String - public let name: String - } - - public struct AuthenticatorSelectionCriteria: Codable { - public init(authenticatorAttachment: String? = nil, residentKey: String, requireResidentKey: Bool, userVerification: String) { - self.authenticatorAttachment = authenticatorAttachment - self.residentKey = residentKey - self.requireResidentKey = requireResidentKey - self.userVerification = userVerification - } +// MARK: - AuthenticatorSelectionCriteria +extension DfnsApi { + public struct AuthenticatorSelectionCriteria: Sendable, Codable { + public let authenticatorAttachment: String? + public let residentKey: String + public let requireResidentKey: Bool + public let userVerification: String + + public init( + authenticatorAttachment: String? = nil, + residentKey: String, + requireResidentKey: Bool, + userVerification: String + ) { + self.authenticatorAttachment = authenticatorAttachment + self.residentKey = residentKey + self.requireResidentKey = requireResidentKey + self.userVerification = userVerification + } + } +} - public let authenticatorAttachment: String? - public let residentKey: String - public let requireResidentKey: Bool - public let userVerification: String - } +// MARK: - Fido2Attestation +extension DfnsApi { + public struct Fido2Attestation: Sendable, Codable { + public let credentialInfo: Fido2AttestationData + public let credentialKind: String - public struct Fido2Attestation: Codable { - public init(credentialInfo: DfnsApi.Fido2AttestationData, credentialKind: String) { + public init( + credentialInfo: Fido2AttestationData, + credentialKind: String + ) { self.credentialInfo = credentialInfo self.credentialKind = credentialKind } + } +} - public let credentialInfo: Fido2AttestationData - public let credentialKind: String - } +// MARK: - Fido2AttestationData +extension DfnsApi { + public struct Fido2AttestationData: Sendable, Codable { + public let attestationData: String + public let clientData: String + public let credId: String - public struct Fido2AttestationData: Codable { - public init(attestationData: String, clientData: String, credId: String) { + public init( + attestationData: String, + clientData: String, + credId: String + ) { self.attestationData = attestationData self.clientData = clientData self.credId = credId } - - public let attestationData: String - public let clientData: String - public let credId: String } } diff --git a/Sources/DfnsSdk/Imported/Passkey.swift b/Sources/DfnsSdk/Imported/Passkey.swift index 58b1a57..e5e8a5c 100644 --- a/Sources/DfnsSdk/Imported/Passkey.swift +++ b/Sources/DfnsSdk/Imported/Passkey.swift @@ -163,28 +163,28 @@ enum PassKeyError: String, Error { case unknown = "UnknownError" } -struct AuthRegistrationResult { +struct AuthRegistrationResult: Sendable { var passkey: PassKeyRegistrationResult var type: PasskeyOperation } -struct AuthAssertionResult { +struct AuthAssertionResult: Sendable { var passkey: PassKeyAssertionResult var type: PasskeyOperation } -struct PassKeyResult { +struct PassKeyResult: Sendable { var registrationResult: PassKeyRegistrationResult? var assertionResult: PassKeyAssertionResult? } -struct PassKeyRegistrationResult { +struct PassKeyRegistrationResult: Sendable { var credentialID: Data var rawAttestationObject: Data var rawClientDataJSON: Data } -struct PassKeyAssertionResult { +struct PassKeyAssertionResult: Sendable { var credentialID: Data var rawAuthenticatorData: Data var rawClientDataJSON: Data @@ -192,7 +192,7 @@ struct PassKeyAssertionResult { var userID: Data } -enum PasskeyOperation { +enum PasskeyOperation: Sendable { case Registration case Assertion } diff --git a/Sources/DfnsSdk/Imported/PasskeyDelegate.swift b/Sources/DfnsSdk/Imported/PasskeyDelegate.swift index ea044f2..037788d 100644 --- a/Sources/DfnsSdk/Imported/PasskeyDelegate.swift +++ b/Sources/DfnsSdk/Imported/PasskeyDelegate.swift @@ -10,7 +10,7 @@ class PasskeyDelegate: NSObject, ASAuthorizationControllerDelegate, ASAuthorizat private var _completion: (_ error: Error?, _ result: PassKeyResult?) -> Void; // Initializes delegate with a completion handler (callback function) - init(completionHandler: @escaping (_ error: Error?, _ result: PassKeyResult?) -> Void) { + init(completionHandler: @escaping @Sendable (_ error: Error?, _ result: PassKeyResult?) -> Void) { self._completion = completionHandler; } diff --git a/Sources/DfnsSdk/PasskeysSigner.swift b/Sources/DfnsSdk/PasskeysSigner.swift index 52dc874..0d1fae9 100644 --- a/Sources/DfnsSdk/PasskeysSigner.swift +++ b/Sources/DfnsSdk/PasskeysSigner.swift @@ -10,104 +10,98 @@ public enum PasskeysSignerError: Error { Wrapper class for the Passkey class imported from the `react-native-passkey library` Converts completion handlers into async functions and make the necessary conversion to work with Dfns API */ +@MainActor public final class PasskeysSigner { - private let passkey = Passkey() - - /** - The relying party ID identifies your application to users, when users create/use passkeys. (Read more [here](https://www.w3.org/TR/webauthn-2/#relying-party)). - It is a valid domain string identifying the WebAuthn Relying Party. In other words, its the domain your application is running on, which will be tied to the passkeys that users create. - We advise to use the root domain, not the full domain (eg `acme.com`, not `app.acme.com` nor `foo.app.acme.com`), that way, passkeys created - by your users can be re-used on other subdomains (eg. on `foo.acme.com` and `bar.acme.com`) in the future. Read more [here](https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredentialCreationOptions#rp). - */ - private let relyingPartyId: String - - public init(relyingPartyId: String) { - self.relyingPartyId = relyingPartyId - } - - public func register(challenge: DfnsApi.UserRegistrationChallenge) async throws -> DfnsApi.Fido2Attestation { - if #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) { - let result = await withCheckedContinuation { continuation in - register(challenge: challenge) { fido2Attestation, exception in - continuation.resume(returning: (fido2Attestation: fido2Attestation, exception: exception)) - } - } - - if result.exception != nil { - throw result.exception! - } - - return result.fido2Attestation! - } else { - throw PasskeysSignerError.unexpected(code: PassKeyError.notSupported.rawValue, message: PassKeyError.notSupported.rawValue, error: nil) - } - } - - private func register(challenge: DfnsApi.UserRegistrationChallenge, completion: @escaping (DfnsApi.Fido2Attestation?, Error?) -> Void) { - let userId = challenge.user.id - let displayName = challenge.user.displayName - let challengeBase64url = Utils.base64URLUnescaped(challenge.challenge) - - passkey.register(self.relyingPartyId, challenge: challengeBase64url, displayName: displayName, userId: userId, securityKey: false, - resolve: { authResult in - let credentialInfo = DfnsApi.Fido2AttestationData( - attestationData: self.extractFromAuthResultValue(authResult, path: ["response", "rawAttestationObject"]), - clientData: self.extractFromAuthResultValue(authResult, path: ["response", "rawClientDataJSON"]), - credId: self.extractFromAuthResultValue(authResult, path: ["credentialID"]) - ) - let fido2Attestation = DfnsApi.Fido2Attestation(credentialInfo: credentialInfo, credentialKind: "Fido2") - completion(fido2Attestation, nil) - }, reject: { code, message, error in - let exception = PasskeysSignerError.unexpected(code: code, message: message, error: error) - completion(nil, exception) - }) - } - - public func sign(challenge: DfnsApi.UserActionChallenge) async throws -> DfnsApi.Fido2Assertion { - if #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) { - let result = await withCheckedContinuation { continuation in - sign(challenge: challenge) { fido2Assertion, exception in - continuation.resume(returning: (fido2Assertion: fido2Assertion, exception: exception)) - } - } - - if result.exception != nil { - throw result.exception! - } - - return result.fido2Assertion! - } else { - throw PasskeysSignerError.unexpected(code: PassKeyError.notSupported.rawValue, message: PassKeyError.notSupported.rawValue, error: nil) - } - } - - private func sign(challenge: DfnsApi.UserActionChallenge, completion: @escaping (DfnsApi.Fido2Assertion?, Error?) -> Void) { - let challengeBase64url = Utils.base64URLUnescaped(challenge.challenge) - - passkey.authenticate(self.relyingPartyId, challenge: challengeBase64url, securityKey: false, resolve: { authResult in - let credentialAssertion = DfnsApi.Fido2AssertionData( - clientData: self.extractFromAuthResultValue(authResult, path: ["response", "rawClientDataJSON"]), - credId: self.extractFromAuthResultValue(authResult, path: ["credentialID"]), - signature: self.extractFromAuthResultValue(authResult, path: ["response", "signature"]), - authenticatorData: self.extractFromAuthResultValue(authResult, path: ["response", "rawAuthenticatorData"]), - userHandle: Utils.base64URLEscape((authResult["userID"] as! String).data(using: .utf8)!.base64EncodedString()) - ) - - let fido2Assertion = DfnsApi.Fido2Assertion(kind: "Fido2", credentialAssertion: credentialAssertion) - - completion(fido2Assertion, nil) - }, reject: { code, message, error in - let exception = PasskeysSignerError.unexpected(code: code, message: message, error: error) - completion(nil, exception) - }) - } - - private func extractFromAuthResultValue(_ authResult: NSDictionary, path: [String]) -> String { - var path = path - if path.count == 1 { - return Utils.base64URLEscape(authResult[path.removeFirst()] as! String) - } else { - return extractFromAuthResultValue(authResult[path.removeFirst()] as! NSDictionary, path: path) - } - } + private let passkey = Passkey() + + /** + The relying party ID identifies your application to users, when users create/use passkeys. (Read more [here](https://www.w3.org/TR/webauthn-2/#relying-party)). + It is a valid domain string identifying the WebAuthn Relying Party. In other words, its the domain your application is running on, which will be tied to the passkeys that users create. + We advise to use the root domain, not the full domain (eg `acme.com`, not `app.acme.com` nor `foo.app.acme.com`), that way, passkeys created + by your users can be re-used on other subdomains (eg. on `foo.acme.com` and `bar.acme.com`) in the future. Read more [here](https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredentialCreationOptions#rp). + */ + private let relyingPartyId: String + + public init(relyingPartyId: String) { + self.relyingPartyId = relyingPartyId + } + + public func register(challenge: DfnsApi.UserRegistrationChallenge) async throws -> DfnsApi.Fido2Attestation { + try await withCheckedThrowingContinuation { continuation in + register(challenge: challenge) { result in + continuation.resume(with: result) + } + } + } + + private func register( + challenge: DfnsApi.UserRegistrationChallenge, + completion: @escaping (Result) -> Void + ) { + let userId = challenge.user.id + let displayName = challenge.user.displayName + let challengeBase64url = Utils.base64URLUnescaped(challenge.challenge) + + passkey.register( + self.relyingPartyId, + challenge: challengeBase64url, + displayName: displayName, + userId: userId, + securityKey: false, + resolve: { authResult in + let credentialInfo = DfnsApi.Fido2AttestationData( + attestationData: self.extractFromAuthResultValue(authResult, path: ["response", "rawAttestationObject"]), + clientData: self.extractFromAuthResultValue(authResult, path: ["response", "rawClientDataJSON"]), + credId: self.extractFromAuthResultValue(authResult, path: ["credentialID"]) + ) + let fido2Attestation = DfnsApi.Fido2Attestation(credentialInfo: credentialInfo, credentialKind: "Fido2") + completion(.success(fido2Attestation)) + }, + reject: { code, message, error in + let exception = PasskeysSignerError.unexpected(code: code, message: message, error: error) + completion(.failure(exception)) + } + ) + } + + public func sign(challenge: DfnsApi.UserActionChallenge) async throws -> DfnsApi.Fido2Assertion { + try await withCheckedThrowingContinuation { continuation in + sign(challenge: challenge) { result in + continuation.resume(with: result) + } + } + } + + private func sign( + challenge: DfnsApi.UserActionChallenge, + completion: @escaping (Result) -> Void + ) { + let challengeBase64url = Utils.base64URLUnescaped(challenge.challenge) + + passkey.authenticate(self.relyingPartyId, challenge: challengeBase64url, securityKey: false, resolve: { authResult in + let credentialAssertion = DfnsApi.Fido2AssertionData( + clientData: self.extractFromAuthResultValue(authResult, path: ["response", "rawClientDataJSON"]), + credId: self.extractFromAuthResultValue(authResult, path: ["credentialID"]), + signature: self.extractFromAuthResultValue(authResult, path: ["response", "signature"]), + authenticatorData: self.extractFromAuthResultValue(authResult, path: ["response", "rawAuthenticatorData"]), + userHandle: Utils.base64URLEscape((authResult["userID"] as! String).data(using: .utf8)!.base64EncodedString()) + ) + + let fido2Assertion = DfnsApi.Fido2Assertion(kind: "Fido2", credentialAssertion: credentialAssertion) + + completion(.success(fido2Assertion)) + }, reject: { code, message, error in + let exception = PasskeysSignerError.unexpected(code: code, message: message, error: error) + completion(.failure(exception)) + }) + } + + private func extractFromAuthResultValue(_ authResult: NSDictionary, path: [String]) -> String { + var path = path + if path.count == 1 { + return Utils.base64URLEscape(authResult[path.removeFirst()] as! String) + } else { + return extractFromAuthResultValue(authResult[path.removeFirst()] as! NSDictionary, path: path) + } + } }