Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Public inits, mutable properties and full Codable support #89

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public struct AuthenticationCredential: Sendable {
public let type: CredentialType
}

extension AuthenticationCredential: Decodable {
extension AuthenticationCredential: Codable {
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)

Expand All @@ -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: 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public struct AuthenticatorAssertionResponse: Sendable {
public let attestationObject: [UInt8]?
}

extension AuthenticatorAssertionResponse: Decodable {
extension AuthenticatorAssertionResponse: Codable {
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)

Expand All @@ -58,6 +58,17 @@ extension AuthenticatorAssertionResponse: Decodable {
userHandle = try container.decodeBytesFromURLEncodedBase64IfPresent(forKey: .userHandle)
attestationObject = try container.decodeBytesFromURLEncodedBase64IfPresent(forKey: .attestationObject)
}

public func encode(to encoder: 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand All @@ -50,15 +50,41 @@ 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=Duration.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
}
Expand All @@ -67,10 +93,10 @@ 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 enum AuthenticatorTransport: String, Equatable, Encodable, Sendable {
public enum AuthenticatorTransport: String, Equatable, Codable, Sendable {
/// Indicates the respective authenticator can be contacted over removable USB.
case usb
/// Indicates the respective authenticator can be contacted over Near Field Communication (NFC).
Expand Down Expand Up @@ -114,6 +140,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)
let type = try container.decode(CredentialType.self,forKey: .type)
let id = try container.decodeBytesFromURLEncodedBase64( forKey: .id)
let transports = try container.decodeIfPresent([AuthenticatorTransport].self,forKey:.transports) ?? []
self.init(type: type, id:id, transports: transports)
}

private enum CodingKeys: String, CodingKey {
case type
Expand All @@ -124,7 +158,7 @@ public struct PublicKeyCredentialDescriptor: Equatable, Encodable, Sendable {

/// The Relying Party may require user verification for some of its operations but not for others, and may use this
/// type to express its needs.
public enum UserVerificationRequirement: String, Encodable, Sendable {
public enum UserVerificationRequirement: String, Codable, Sendable {
/// The Relying Party requires user verification for the operation and will fail the overall ceremony if the
/// user wasn't verified.
case required
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
/// Options to specify the Relying Party's preference regarding attestation conveyance during credential generation.
///
/// Currently only supports `none`.
public enum AttestationConveyancePreference: String, Encodable, Sendable {
public enum AttestationConveyancePreference: String, Codable, Sendable {
/// Indicates the Relying Party is not interested in authenticator attestation.
case none
// case indirect
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,21 @@ public struct AuthenticatorAttestationResponse: Sendable {
public let attestationObject: [UInt8]
}

extension AuthenticatorAttestationResponse: Decodable {
extension AuthenticatorAttestationResponse: Codable {
public init(from decoder: 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: 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand All @@ -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: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
Expand All @@ -57,6 +57,29 @@ 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 = Duration.milliseconds(timeout)
}
self.attestation = try values.decode(AttestationConveyancePreference.self, forKey:.attestation)
}

public init(challenge: [UInt8],user: PublicKeyCredentialUserEntity,relyingParty: PublicKeyCredentialRelyingPartyEntity,publicKeyCredentialParameters: [PublicKeyCredentialParameters],
dscreve marked this conversation as resolved.
Show resolved Hide resolved
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
Expand All @@ -70,7 +93,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
Expand All @@ -87,6 +110,18 @@ 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)
let type = try container.decode(CredentialType.self,forKey: .type)
let alg = try container.decode(COSEAlgorithmIdentifier.self, forKey: .alg)
self.init(type:type,alg:alg)
}

private enum CodingKeys: String, CodingKey {
case type
case alg
}
dscreve marked this conversation as resolved.
Show resolved Hide resolved
}

extension Array where Element == PublicKeyCredentialParameters {
Expand All @@ -103,22 +138,31 @@ 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

/// 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 init(_ src : PublicKeyCredentialRelyingPartyEntity) {
self.id = src.id
self.name = src.name
}

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)
/// The PublicKeyCredentialUserEntity dictionary is used to supply additional user account attributes when
/// 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.
///
Expand Down Expand Up @@ -149,6 +193,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)
let id = try container.decodeBytesFromURLEncodedBase64(forKey: .id)
let name = try container.decode(String.self, forKey: .name)
let displayName = try container.decode(String.self, forKey: .displayName)
self.init(id: id, name: name, displayName: displayName)
}


private enum CodingKeys: String, CodingKey {
case id
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public struct RegistrationCredential: Sendable {
public let attestationResponse: AuthenticatorAttestationResponse
}

extension RegistrationCredential: Decodable {
extension RegistrationCredential: Codable {
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)

Expand All @@ -47,6 +47,15 @@ extension RegistrationCredential: Decodable {
self.rawID = rawID
attestationResponse = try container.decode(AuthenticatorAttestationResponse.self, forKey: .attestationResponse)
}

public func encode(to encoder: 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

import Foundation

extension KeyedDecodingContainer {
public extension KeyedDecodingContainer {
dscreve marked this conversation as resolved.
Show resolved Hide resolved
func decodeBytesFromURLEncodedBase64(forKey key: KeyedDecodingContainer.Key) throws -> [UInt8] {
guard let bytes = try decode(
URLEncodedBase64.self,
Expand Down