diff --git a/Sources/WebAuthn/Ceremonies/Authentication/AuthenticationCredential.swift b/Sources/WebAuthn/Ceremonies/Authentication/AuthenticationCredential.swift index 6237cc6..744423e 100644 --- a/Sources/WebAuthn/Ceremonies/Authentication/AuthenticationCredential.swift +++ b/Sources/WebAuthn/Ceremonies/Authentication/AuthenticationCredential.swift @@ -34,7 +34,7 @@ public struct AuthenticationCredential: Sendable { public let type: CredentialType } -extension AuthenticationCredential: Decodable { +extension AuthenticationCredential: Codable { public init(from decoder: any Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) @@ -44,6 +44,17 @@ extension AuthenticationCredential: Decodable { authenticatorAttachment = try container.decodeIfPresent(AuthenticatorAttachment.self, forKey: .authenticatorAttachment) type = try container.decode(CredentialType.self, forKey: .type) } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(id, forKey: .id) + try container.encode(rawID.base64URLEncodedString(), forKey: .rawID) + try container.encode(response, forKey: .response) + try container.encodeIfPresent(authenticatorAttachment, forKey: .authenticatorAttachment) + try container.encode(type, forKey: .type) + } + private enum CodingKeys: String, CodingKey { case id diff --git a/Sources/WebAuthn/Ceremonies/Authentication/AuthenticatorAssertionResponse.swift b/Sources/WebAuthn/Ceremonies/Authentication/AuthenticatorAssertionResponse.swift index cf9580b..2daa5df 100644 --- a/Sources/WebAuthn/Ceremonies/Authentication/AuthenticatorAssertionResponse.swift +++ b/Sources/WebAuthn/Ceremonies/Authentication/AuthenticatorAssertionResponse.swift @@ -48,7 +48,7 @@ public struct AuthenticatorAssertionResponse: Sendable { public let attestationObject: [UInt8]? } -extension AuthenticatorAssertionResponse: Decodable { +extension AuthenticatorAssertionResponse: Codable { public init(from decoder: any Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) @@ -58,6 +58,17 @@ extension AuthenticatorAssertionResponse: Decodable { userHandle = try container.decodeBytesFromURLEncodedBase64IfPresent(forKey: .userHandle) attestationObject = try container.decodeBytesFromURLEncodedBase64IfPresent(forKey: .attestationObject) } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(clientDataJSON.base64URLEncodedString(), forKey: .clientDataJSON) + try container.encode(authenticatorData.base64URLEncodedString(), forKey: .authenticatorData) + try container.encode(signature.base64URLEncodedString(), forKey: .signature) + try container.encodeIfPresent(userHandle?.base64URLEncodedString(), forKey: .userHandle) + try container.encodeIfPresent(attestationObject?.base64URLEncodedString(), forKey: .attestationObject) + } + private enum CodingKeys: String, CodingKey { case clientDataJSON diff --git a/Sources/WebAuthn/Ceremonies/Authentication/PublicKeyCredentialRequestOptions.swift b/Sources/WebAuthn/Ceremonies/Authentication/PublicKeyCredentialRequestOptions.swift index 737d905..afe8980 100644 --- a/Sources/WebAuthn/Ceremonies/Authentication/PublicKeyCredentialRequestOptions.swift +++ b/Sources/WebAuthn/Ceremonies/Authentication/PublicKeyCredentialRequestOptions.swift @@ -18,30 +18,30 @@ import Foundation /// When encoding using `Encodable`, the byte arrays are encoded as base64url. /// /// - SeeAlso: https://www.w3.org/TR/webauthn-2/#dictionary-assertion-options -public struct PublicKeyCredentialRequestOptions: Encodable, Sendable { +public struct PublicKeyCredentialRequestOptions: Codable, Sendable { /// A challenge that the authenticator signs, along with other data, when producing an authentication assertion /// /// When encoding using `Encodable` this is encoded as base64url. - public let challenge: [UInt8] + public var challenge: [UInt8] /// A time, in seconds, that the caller is willing to wait for the call to complete. This is treated as a /// hint, and may be overridden by the client. /// /// - Note: When encoded, this value is represented in milleseconds as a ``UInt32``. /// See https://www.w3.org/TR/webauthn-2/#dictionary-assertion-options - public let timeout: Duration? + public var timeout: Duration? /// The ID of the Relying Party making the request. /// /// This is configured on ``WebAuthnManager`` before its ``WebAuthnManager/beginAuthentication(timeout:allowCredentials:userVerification:)`` method is called. /// - Note: When encoded, this field appears as `rpId` to match the expectations of `navigator.credentials.get()`. - public let relyingPartyID: String + public var relyingPartyID: String /// Optionally used by the client to find authenticators eligible for this authentication ceremony. - public let allowCredentials: [PublicKeyCredentialDescriptor]? + public var allowCredentials: [PublicKeyCredentialDescriptor]? /// Specifies whether the user should be verified during the authentication ceremony. - public let userVerification: UserVerificationRequirement? + public var userVerification: UserVerificationRequirement? // let extensions: [String: Any] @@ -50,15 +50,42 @@ public struct PublicKeyCredentialRequestOptions: Encodable, Sendable { try container.encode(challenge.base64URLEncodedString(), forKey: .challenge) try container.encodeIfPresent(timeout?.milliseconds, forKey: .timeout) - try container.encode(relyingPartyID, forKey: .rpID) + try container.encode(relyingPartyID, forKey: .relyingPartyID) try container.encodeIfPresent(allowCredentials, forKey: .allowCredentials) try container.encodeIfPresent(userVerification, forKey: .userVerification) } + + public init( + challenge: [UInt8], + timeout: Duration?, + relyingPartyID: String, + allowCredentials: [PublicKeyCredentialDescriptor]?, + userVerification: UserVerificationRequirement? + ) { + self.challenge = challenge + self.timeout = timeout + self.relyingPartyID = relyingPartyID + self.allowCredentials = allowCredentials + self.userVerification = userVerification + } + + public init(from decoder: any Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + + self.challenge = try values.decodeBytesFromURLEncodedBase64(forKey: .challenge) + + if let timeout = try values.decodeIfPresent(UInt32.self, forKey: .timeout) { + self.timeout = .milliseconds(timeout) + } + self.relyingPartyID = try values.decode(String.self, forKey: .relyingPartyID) + self.allowCredentials = try values.decodeIfPresent([PublicKeyCredentialDescriptor].self, forKey: .allowCredentials) + self.userVerification = try values.decodeIfPresent(UserVerificationRequirement.self, forKey: .userVerification) + } private enum CodingKeys: String, CodingKey { case challenge case timeout - case rpID = "rpId" + case relyingPartyID = "rpId" case allowCredentials case userVerification } @@ -67,7 +94,7 @@ public struct PublicKeyCredentialRequestOptions: Encodable, Sendable { /// Information about a generated credential. /// /// When encoding using `Encodable`, `id` is encoded as base64url. -public struct PublicKeyCredentialDescriptor: Equatable, Encodable, Sendable { +public struct PublicKeyCredentialDescriptor: Equatable, Codable, Sendable { /// Defines hints as to how clients might communicate with a particular authenticator in order to obtain an /// assertion for a specific credential public struct AuthenticatorTransport: UnreferencedStringEnumeration, Sendable { @@ -119,6 +146,14 @@ public struct PublicKeyCredentialDescriptor: Equatable, Encodable, Sendable { try container.encode(id.base64URLEncodedString(), forKey: .id) try container.encodeIfPresent(transports, forKey: .transports) } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.type = try container.decode(CredentialType.self, forKey: .type) + self.id = try container.decodeBytesFromURLEncodedBase64(forKey: .id) + self.transports = try container.decodeIfPresent([AuthenticatorTransport].self, forKey: .transports) ?? [] + } private enum CodingKeys: String, CodingKey { case type diff --git a/Sources/WebAuthn/Ceremonies/Registration/AuthenticatorAttestationResponse.swift b/Sources/WebAuthn/Ceremonies/Registration/AuthenticatorAttestationResponse.swift index b12d3b7..343dc31 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/AuthenticatorAttestationResponse.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/AuthenticatorAttestationResponse.swift @@ -29,13 +29,21 @@ public struct AuthenticatorAttestationResponse: Sendable { public let attestationObject: [UInt8] } -extension AuthenticatorAttestationResponse: Decodable { +extension AuthenticatorAttestationResponse: Codable { public init(from decoder: any Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) clientDataJSON = try container.decodeBytesFromURLEncodedBase64(forKey: .clientDataJSON) attestationObject = try container.decodeBytesFromURLEncodedBase64(forKey: .attestationObject) } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(clientDataJSON.base64URLEncodedString(), forKey: .clientDataJSON) + try container.encode(attestationObject.base64URLEncodedString(), forKey: .attestationObject) + } + private enum CodingKeys: String, CodingKey { case clientDataJSON diff --git a/Sources/WebAuthn/Ceremonies/Registration/PublicKeyCredentialCreationOptions.swift b/Sources/WebAuthn/Ceremonies/Registration/PublicKeyCredentialCreationOptions.swift index 9fdfabf..2d0976b 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/PublicKeyCredentialCreationOptions.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/PublicKeyCredentialCreationOptions.swift @@ -19,7 +19,7 @@ import Foundation /// `Encodable` byte arrays are base64url encoded. /// /// - SeeAlso: https://www.w3.org/TR/webauthn-2/#dictionary-makecredentialoptions -public struct PublicKeyCredentialCreationOptions: Encodable, Sendable { +public struct PublicKeyCredentialCreationOptions: Codable, Sendable { /// A byte array randomly generated by the Relying Party. Should be at least 16 bytes long to ensure sufficient /// entropy. /// @@ -28,24 +28,24 @@ public struct PublicKeyCredentialCreationOptions: Encodable, Sendable { public let challenge: [UInt8] /// Contains names and an identifier for the user account performing the registration - public let user: PublicKeyCredentialUserEntity + public var user: PublicKeyCredentialUserEntity /// Contains a name and an identifier for the Relying Party responsible for the request - public let relyingParty: PublicKeyCredentialRelyingPartyEntity + public var relyingParty: PublicKeyCredentialRelyingPartyEntity /// A list of key types and signature algorithms the Relying Party supports. Ordered from most preferred to least /// preferred. - public let publicKeyCredentialParameters: [PublicKeyCredentialParameters] + public var publicKeyCredentialParameters: [PublicKeyCredentialParameters] /// A time, in seconds, that the caller is willing to wait for the call to complete. This is treated as a /// hint, and may be overridden by the client. /// /// - Note: When encoded, this value is represented in milleseconds as a ``UInt32``. - public let timeout: Duration? + public var timeout: Duration? /// Sets the Relying Party's preference for attestation conveyance. At the time of writing only `none` is /// supported. - public let attestation: AttestationConveyancePreference + public var attestation: AttestationConveyancePreference public func encode(to encoder: any Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) @@ -57,6 +57,35 @@ public struct PublicKeyCredentialCreationOptions: Encodable, Sendable { try container.encodeIfPresent(timeout?.milliseconds, forKey: .timeout) try container.encode(attestation, forKey: .attestation) } + + public init(from decoder: any Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + + self.challenge = try values.decodeBytesFromURLEncodedBase64(forKey: .challenge) + self.user = try values.decode(PublicKeyCredentialUserEntity.self, forKey: .user) + self.relyingParty = try values.decode(PublicKeyCredentialRelyingPartyEntity.self, forKey: .relyingParty) + self.publicKeyCredentialParameters = try values.decode([PublicKeyCredentialParameters].self, forKey: .publicKeyCredentialParameters) + if let timeout = try values.decodeIfPresent(UInt32.self, forKey: .timeout) { + self.timeout = .milliseconds(timeout) + } + self.attestation = try values.decode(AttestationConveyancePreference.self, forKey: .attestation) + } + + public init( + challenge: [UInt8], + user: PublicKeyCredentialUserEntity, + relyingParty: PublicKeyCredentialRelyingPartyEntity, + publicKeyCredentialParameters: [PublicKeyCredentialParameters], + timeout: Duration?, + attestation: AttestationConveyancePreference + ) { + self.challenge = challenge + self.user = user + self.relyingParty = relyingParty + self.publicKeyCredentialParameters = publicKeyCredentialParameters + self.timeout = timeout + self.attestation = attestation + } private enum CodingKeys: String, CodingKey { case challenge @@ -70,7 +99,7 @@ public struct PublicKeyCredentialCreationOptions: Encodable, Sendable { // MARK: - Credential parameters /// From §5.3 (https://w3c.github.io/TR/webauthn/#dictionary-credential-params) -public struct PublicKeyCredentialParameters: Equatable, Encodable, Sendable { +public struct PublicKeyCredentialParameters: Equatable, Codable, Sendable { /// The type of credential to be created. At the time of writing always ``CredentialType/publicKey``. public let type: CredentialType /// The cryptographic signature algorithm with which the newly generated credential will be used, and thus also @@ -87,6 +116,13 @@ public struct PublicKeyCredentialParameters: Equatable, Encodable, Sendable { self.type = type self.alg = alg } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.type = try container.decode(CredentialType.self, forKey: .type) + self.alg = try container.decode(COSEAlgorithmIdentifier.self, forKey: .alg) + } } extension Array where Element == PublicKeyCredentialParameters { @@ -103,14 +139,18 @@ extension Array where Element == PublicKeyCredentialParameters { /// From §5.4.2 (https://www.w3.org/TR/webauthn/#sctn-rp-credential-params). /// The PublicKeyCredentialRelyingPartyEntity dictionary is used to supply additional Relying Party attributes when /// creating a new credential. -public struct PublicKeyCredentialRelyingPartyEntity: Encodable, Sendable { +public struct PublicKeyCredentialRelyingPartyEntity: Codable, Sendable { /// A unique identifier for the Relying Party entity. - public let id: String + public var id: String /// A human-readable identifier for the Relying Party, intended only for display. For example, "ACME Corporation", /// "Wonderful Widgets, Inc." or "ОАО Примертех". - public let name: String + public var name: String + public init(id: String, name: String) { + self.id = id + self.name = name + } } /// From §5.4.3 (https://www.w3.org/TR/webauthn/#dictionary-user-credential-params) @@ -118,7 +158,7 @@ public struct PublicKeyCredentialRelyingPartyEntity: Encodable, Sendable { /// creating a new credential. /// /// When encoding using `Encodable`, `id` is base64url encoded. -public struct PublicKeyCredentialUserEntity: Encodable, Sendable { +public struct PublicKeyCredentialUserEntity: Codable, Sendable { /// Generated by the Relying Party, unique to the user account, and must not contain personally identifying /// information about the user. /// @@ -149,6 +189,15 @@ public struct PublicKeyCredentialUserEntity: Encodable, Sendable { try container.encode(name, forKey: .name) try container.encode(displayName, forKey: .displayName) } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.id = try container.decodeBytesFromURLEncodedBase64(forKey: .id) + self.name = try container.decode(String.self, forKey: .name) + self.displayName = try container.decode(String.self, forKey: .displayName) + } + private enum CodingKeys: String, CodingKey { case id diff --git a/Sources/WebAuthn/Ceremonies/Registration/RegistrationCredential.swift b/Sources/WebAuthn/Ceremonies/Registration/RegistrationCredential.swift index 2bc3f9f..02beecd 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/RegistrationCredential.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/RegistrationCredential.swift @@ -31,7 +31,7 @@ public struct RegistrationCredential: Sendable { public let attestationResponse: AuthenticatorAttestationResponse } -extension RegistrationCredential: Decodable { +extension RegistrationCredential: Codable { public init(from decoder: any Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) @@ -47,6 +47,15 @@ extension RegistrationCredential: Decodable { self.rawID = rawID attestationResponse = try container.decode(AuthenticatorAttestationResponse.self, forKey: .attestationResponse) } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(id, forKey: .id) + try container.encode(rawID.base64URLEncodedString(), forKey: .rawID) + try container.encode(attestationResponse, forKey: .attestationResponse) + } + private enum CodingKeys: String, CodingKey { case id diff --git a/Sources/WebAuthn/Ceremonies/Shared/COSE/COSEAlgorithmIdentifier.swift b/Sources/WebAuthn/Ceremonies/Shared/COSE/COSEAlgorithmIdentifier.swift index adf2c2d..c25b2b6 100644 --- a/Sources/WebAuthn/Ceremonies/Shared/COSE/COSEAlgorithmIdentifier.swift +++ b/Sources/WebAuthn/Ceremonies/Shared/COSE/COSEAlgorithmIdentifier.swift @@ -17,7 +17,7 @@ import Crypto /// COSEAlgorithmIdentifier From §5.10.5. A number identifying a cryptographic algorithm. The algorithm /// identifiers SHOULD be values registered in the IANA COSE Algorithms registry /// [https://www.w3.org/TR/webauthn/#biblio-iana-cose-algs-reg], for instance, -7 for "ES256" and -257 for "RS256". -public enum COSEAlgorithmIdentifier: Int, RawRepresentable, CaseIterable, Encodable, Sendable { +public enum COSEAlgorithmIdentifier: Int, RawRepresentable, CaseIterable, Codable, Sendable { /// AlgES256 ECDSA with SHA-256 case algES256 = -7 /// AlgES384 ECDSA with SHA-384