Skip to content

Commit b41c23b

Browse files
committed
Public inits, mutable properties and full Codable support
1 parent 0f31a51 commit b41c23b

11 files changed

+161
-30
lines changed

Sources/WebAuthn/Ceremonies/Authentication/AuthenticationCredential.swift

+12-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public struct AuthenticationCredential: Sendable {
3434
public let type: CredentialType
3535
}
3636

37-
extension AuthenticationCredential: Decodable {
37+
extension AuthenticationCredential: Codable {
3838
public init(from decoder: Decoder) throws {
3939
let container = try decoder.container(keyedBy: CodingKeys.self)
4040

@@ -44,6 +44,17 @@ extension AuthenticationCredential: Decodable {
4444
authenticatorAttachment = try container.decodeIfPresent(AuthenticatorAttachment.self, forKey: .authenticatorAttachment)
4545
type = try container.decode(CredentialType.self, forKey: .type)
4646
}
47+
48+
public func encode(to encoder: Encoder) throws {
49+
var container = encoder.container(keyedBy: CodingKeys.self)
50+
51+
try container.encode(id, forKey: .id)
52+
try container.encode(rawID.base64URLEncodedString(), forKey: .rawID)
53+
try container.encode(response, forKey: .response)
54+
try container.encodeIfPresent(authenticatorAttachment, forKey: .authenticatorAttachment)
55+
try container.encode(type, forKey: .type)
56+
}
57+
4758

4859
private enum CodingKeys: String, CodingKey {
4960
case id

Sources/WebAuthn/Ceremonies/Authentication/AuthenticatorAssertionResponse.swift

+12-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public struct AuthenticatorAssertionResponse: Sendable {
4848
public let attestationObject: [UInt8]?
4949
}
5050

51-
extension AuthenticatorAssertionResponse: Decodable {
51+
extension AuthenticatorAssertionResponse: Codable {
5252
public init(from decoder: Decoder) throws {
5353
let container = try decoder.container(keyedBy: CodingKeys.self)
5454

@@ -58,6 +58,17 @@ extension AuthenticatorAssertionResponse: Decodable {
5858
userHandle = try container.decodeBytesFromURLEncodedBase64IfPresent(forKey: .userHandle)
5959
attestationObject = try container.decodeBytesFromURLEncodedBase64IfPresent(forKey: .attestationObject)
6060
}
61+
62+
public func encode(to encoder: Encoder) throws {
63+
var container = encoder.container(keyedBy: CodingKeys.self)
64+
65+
try container.encode(clientDataJSON.base64URLEncodedString(), forKey: .clientDataJSON)
66+
try container.encode(authenticatorData.base64URLEncodedString(), forKey: .authenticatorData)
67+
try container.encode(signature.base64URLEncodedString(), forKey: .signature)
68+
try container.encodeIfPresent(userHandle?.base64URLEncodedString(),forKey: .userHandle)
69+
try container.encodeIfPresent(attestationObject?.base64URLEncodedString(),forKey: .attestationObject)
70+
}
71+
6172

6273
private enum CodingKeys: String, CodingKey {
6374
case clientDataJSON

Sources/WebAuthn/Ceremonies/Authentication/PublicKeyCredentialRequestOptions.swift

+45-11
Original file line numberDiff line numberDiff line change
@@ -18,30 +18,30 @@ import Foundation
1818
/// When encoding using `Encodable`, the byte arrays are encoded as base64url.
1919
///
2020
/// - SeeAlso: https://www.w3.org/TR/webauthn-2/#dictionary-assertion-options
21-
public struct PublicKeyCredentialRequestOptions: Encodable, Sendable {
21+
public struct PublicKeyCredentialRequestOptions: Codable, Sendable {
2222
/// A challenge that the authenticator signs, along with other data, when producing an authentication assertion
2323
///
2424
/// When encoding using `Encodable` this is encoded as base64url.
25-
public let challenge: [UInt8]
25+
public var challenge: [UInt8]
2626

2727
/// A time, in seconds, that the caller is willing to wait for the call to complete. This is treated as a
2828
/// hint, and may be overridden by the client.
2929
///
3030
/// - Note: When encoded, this value is represented in milleseconds as a ``UInt32``.
3131
/// See https://www.w3.org/TR/webauthn-2/#dictionary-assertion-options
32-
public let timeout: Duration?
32+
public var timeout: Duration?
3333

3434
/// The ID of the Relying Party making the request.
3535
///
3636
/// This is configured on ``WebAuthnManager`` before its ``WebAuthnManager/beginAuthentication(timeout:allowCredentials:userVerification:)`` method is called.
3737
/// - Note: When encoded, this field appears as `rpId` to match the expectations of `navigator.credentials.get()`.
38-
public let relyingPartyID: String
38+
public var relyingPartyID: String
3939

4040
/// Optionally used by the client to find authenticators eligible for this authentication ceremony.
41-
public let allowCredentials: [PublicKeyCredentialDescriptor]?
41+
public var allowCredentials: [PublicKeyCredentialDescriptor]?
4242

4343
/// Specifies whether the user should be verified during the authentication ceremony.
44-
public let userVerification: UserVerificationRequirement?
44+
public var userVerification: UserVerificationRequirement?
4545

4646
// let extensions: [String: Any]
4747

@@ -50,15 +50,41 @@ public struct PublicKeyCredentialRequestOptions: Encodable, Sendable {
5050

5151
try container.encode(challenge.base64URLEncodedString(), forKey: .challenge)
5252
try container.encodeIfPresent(timeout?.milliseconds, forKey: .timeout)
53-
try container.encode(relyingPartyID, forKey: .rpID)
53+
try container.encode(relyingPartyID, forKey: .relyingPartyID)
5454
try container.encodeIfPresent(allowCredentials, forKey: .allowCredentials)
5555
try container.encodeIfPresent(userVerification, forKey: .userVerification)
5656
}
57+
58+
public init(
59+
challenge: [UInt8],
60+
timeout: Duration?,
61+
relyingPartyID: String,
62+
allowCredentials: [PublicKeyCredentialDescriptor]?,
63+
userVerification: UserVerificationRequirement?) {
64+
self.challenge = challenge
65+
self.timeout = timeout
66+
self.relyingPartyID = relyingPartyID
67+
self.allowCredentials = allowCredentials
68+
self.userVerification = userVerification
69+
}
70+
71+
public init(from decoder: any Decoder) throws {
72+
let values = try decoder.container(keyedBy: CodingKeys.self)
73+
74+
self.challenge = try values.decodeBytesFromURLEncodedBase64(forKey: .challenge)
75+
76+
if let timeout = try values.decodeIfPresent(UInt32.self, forKey:.timeout) {
77+
self.timeout=Duration.milliseconds(timeout)
78+
}
79+
self.relyingPartyID=try values.decode(String.self, forKey:.relyingPartyID)
80+
self.allowCredentials=try values.decodeIfPresent([PublicKeyCredentialDescriptor].self,forKey: .allowCredentials)
81+
self.userVerification=try values.decodeIfPresent(UserVerificationRequirement.self,forKey: .userVerification)
82+
}
5783

5884
private enum CodingKeys: String, CodingKey {
5985
case challenge
6086
case timeout
61-
case rpID = "rpId"
87+
case relyingPartyID = "rpId"
6288
case allowCredentials
6389
case userVerification
6490
}
@@ -67,10 +93,10 @@ public struct PublicKeyCredentialRequestOptions: Encodable, Sendable {
6793
/// Information about a generated credential.
6894
///
6995
/// When encoding using `Encodable`, `id` is encoded as base64url.
70-
public struct PublicKeyCredentialDescriptor: Equatable, Encodable, Sendable {
96+
public struct PublicKeyCredentialDescriptor: Equatable, Codable, Sendable {
7197
/// Defines hints as to how clients might communicate with a particular authenticator in order to obtain an
7298
/// assertion for a specific credential
73-
public enum AuthenticatorTransport: String, Equatable, Encodable, Sendable {
99+
public enum AuthenticatorTransport: String, Equatable, Codable, Sendable {
74100
/// Indicates the respective authenticator can be contacted over removable USB.
75101
case usb
76102
/// Indicates the respective authenticator can be contacted over Near Field Communication (NFC).
@@ -114,6 +140,14 @@ public struct PublicKeyCredentialDescriptor: Equatable, Encodable, Sendable {
114140
try container.encode(id.base64URLEncodedString(), forKey: .id)
115141
try container.encodeIfPresent(transports, forKey: .transports)
116142
}
143+
144+
public init(from decoder: any Decoder) throws {
145+
let container = try decoder.container(keyedBy: CodingKeys.self)
146+
let type = try container.decode(CredentialType.self,forKey: .type)
147+
let id = try container.decodeBytesFromURLEncodedBase64( forKey: .id)
148+
let transports = try container.decodeIfPresent([AuthenticatorTransport].self,forKey:.transports) ?? []
149+
self.init(type: type, id:id, transports: transports)
150+
}
117151

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

125159
/// The Relying Party may require user verification for some of its operations but not for others, and may use this
126160
/// type to express its needs.
127-
public enum UserVerificationRequirement: String, Encodable, Sendable {
161+
public enum UserVerificationRequirement: String, Codable, Sendable {
128162
/// The Relying Party requires user verification for the operation and will fail the overall ceremony if the
129163
/// user wasn't verified.
130164
case required

Sources/WebAuthn/Ceremonies/Registration/AttestationConveyancePreference.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
/// Options to specify the Relying Party's preference regarding attestation conveyance during credential generation.
1515
///
1616
/// Currently only supports `none`.
17-
public enum AttestationConveyancePreference: String, Encodable, Sendable {
17+
public enum AttestationConveyancePreference: String, Codable, Sendable {
1818
/// Indicates the Relying Party is not interested in authenticator attestation.
1919
case none
2020
// case indirect

Sources/WebAuthn/Ceremonies/Registration/AuthenticatorAttestationResponse.swift

+9-1
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,21 @@ public struct AuthenticatorAttestationResponse: Sendable {
2929
public let attestationObject: [UInt8]
3030
}
3131

32-
extension AuthenticatorAttestationResponse: Decodable {
32+
extension AuthenticatorAttestationResponse: Codable {
3333
public init(from decoder: Decoder) throws {
3434
let container = try decoder.container(keyedBy: CodingKeys.self)
3535

3636
clientDataJSON = try container.decodeBytesFromURLEncodedBase64(forKey: .clientDataJSON)
3737
attestationObject = try container.decodeBytesFromURLEncodedBase64(forKey: .attestationObject)
3838
}
39+
40+
public func encode(to encoder: Encoder) throws {
41+
var container = encoder.container(keyedBy: CodingKeys.self)
42+
43+
try container.encode(clientDataJSON.base64URLEncodedString(), forKey: .clientDataJSON)
44+
try container.encode(attestationObject.base64URLEncodedString(), forKey: .attestationObject)
45+
}
46+
3947

4048
private enum CodingKeys: String, CodingKey {
4149
case clientDataJSON

Sources/WebAuthn/Ceremonies/Registration/PublicKeyCredentialCreationOptions.swift

+63-10
Original file line numberDiff line numberDiff line change
@@ -19,33 +19,33 @@ import Foundation
1919
/// `Encodable` byte arrays are base64url encoded.
2020
///
2121
/// - SeeAlso: https://www.w3.org/TR/webauthn-2/#dictionary-makecredentialoptions
22-
public struct PublicKeyCredentialCreationOptions: Encodable, Sendable {
22+
public struct PublicKeyCredentialCreationOptions: Codable, Sendable {
2323
/// A byte array randomly generated by the Relying Party. Should be at least 16 bytes long to ensure sufficient
2424
/// entropy.
2525
///
2626
/// The Relying Party should store the challenge temporarily until the registration flow is complete. When
2727
/// encoding using `Encodable`, the challenge is base64url encoded.
28-
public let challenge: [UInt8]
28+
public var challenge: [UInt8]
2929

3030
/// Contains names and an identifier for the user account performing the registration
31-
public let user: PublicKeyCredentialUserEntity
31+
public var user: PublicKeyCredentialUserEntity
3232

3333
/// Contains a name and an identifier for the Relying Party responsible for the request
34-
public let relyingParty: PublicKeyCredentialRelyingPartyEntity
34+
public var relyingParty: PublicKeyCredentialRelyingPartyEntity
3535

3636
/// A list of key types and signature algorithms the Relying Party supports. Ordered from most preferred to least
3737
/// preferred.
38-
public let publicKeyCredentialParameters: [PublicKeyCredentialParameters]
38+
public var publicKeyCredentialParameters: [PublicKeyCredentialParameters]
3939

4040
/// A time, in seconds, that the caller is willing to wait for the call to complete. This is treated as a
4141
/// hint, and may be overridden by the client.
4242
///
4343
/// - Note: When encoded, this value is represented in milleseconds as a ``UInt32``.
44-
public let timeout: Duration?
44+
public var timeout: Duration?
4545

4646
/// Sets the Relying Party's preference for attestation conveyance. At the time of writing only `none` is
4747
/// supported.
48-
public let attestation: AttestationConveyancePreference
48+
public var attestation: AttestationConveyancePreference
4949

5050
public func encode(to encoder: Encoder) throws {
5151
var container = encoder.container(keyedBy: CodingKeys.self)
@@ -57,6 +57,29 @@ public struct PublicKeyCredentialCreationOptions: Encodable, Sendable {
5757
try container.encodeIfPresent(timeout?.milliseconds, forKey: .timeout)
5858
try container.encode(attestation, forKey: .attestation)
5959
}
60+
61+
public init(from decoder: any Decoder) throws {
62+
let values = try decoder.container(keyedBy: CodingKeys.self)
63+
64+
self.challenge = try values.decodeBytesFromURLEncodedBase64(forKey: .challenge)
65+
self.user = try values.decode(PublicKeyCredentialUserEntity.self, forKey: .user)
66+
self.relyingParty = try values.decode(PublicKeyCredentialRelyingPartyEntity.self, forKey:.relyingParty)
67+
self.publicKeyCredentialParameters = try values.decode([PublicKeyCredentialParameters].self, forKey:.publicKeyCredentialParameters)
68+
if let timeout = try values.decodeIfPresent(UInt32.self,forKey:.timeout) {
69+
self.timeout = Duration.milliseconds(timeout)
70+
}
71+
self.attestation = try values.decode(AttestationConveyancePreference.self, forKey:.attestation)
72+
}
73+
74+
public init(challenge: [UInt8],user: PublicKeyCredentialUserEntity,relyingParty: PublicKeyCredentialRelyingPartyEntity,publicKeyCredentialParameters: [PublicKeyCredentialParameters],
75+
timeout: Duration?,attestation: AttestationConveyancePreference) {
76+
self.challenge = challenge
77+
self.user = user
78+
self.relyingParty = relyingParty
79+
self.publicKeyCredentialParameters = publicKeyCredentialParameters
80+
self.timeout = timeout
81+
self.attestation = attestation
82+
}
6083

6184
private enum CodingKeys: String, CodingKey {
6285
case challenge
@@ -70,7 +93,7 @@ public struct PublicKeyCredentialCreationOptions: Encodable, Sendable {
7093

7194
// MARK: - Credential parameters
7295
/// From §5.3 (https://w3c.github.io/TR/webauthn/#dictionary-credential-params)
73-
public struct PublicKeyCredentialParameters: Equatable, Encodable, Sendable {
96+
public struct PublicKeyCredentialParameters: Equatable, Codable, Sendable {
7497
/// The type of credential to be created. At the time of writing always ``CredentialType/publicKey``.
7598
public let type: CredentialType
7699
/// The cryptographic signature algorithm with which the newly generated credential will be used, and thus also
@@ -87,6 +110,18 @@ public struct PublicKeyCredentialParameters: Equatable, Encodable, Sendable {
87110
self.type = type
88111
self.alg = alg
89112
}
113+
114+
public init(from decoder: any Decoder) throws {
115+
let container = try decoder.container(keyedBy: CodingKeys.self)
116+
let type = try container.decode(CredentialType.self,forKey: .type)
117+
let alg = try container.decode(COSEAlgorithmIdentifier.self, forKey: .alg)
118+
self.init(type:type,alg:alg)
119+
}
120+
121+
private enum CodingKeys: String, CodingKey {
122+
case type
123+
case alg
124+
}
90125
}
91126

92127
extension Array where Element == PublicKeyCredentialParameters {
@@ -103,22 +138,31 @@ extension Array where Element == PublicKeyCredentialParameters {
103138
/// From §5.4.2 (https://www.w3.org/TR/webauthn/#sctn-rp-credential-params).
104139
/// The PublicKeyCredentialRelyingPartyEntity dictionary is used to supply additional Relying Party attributes when
105140
/// creating a new credential.
106-
public struct PublicKeyCredentialRelyingPartyEntity: Encodable, Sendable {
141+
public struct PublicKeyCredentialRelyingPartyEntity: Codable, Sendable {
107142
/// A unique identifier for the Relying Party entity.
108143
public let id: String
109144

110145
/// A human-readable identifier for the Relying Party, intended only for display. For example, "ACME Corporation",
111146
/// "Wonderful Widgets, Inc." or "ОАО Примертех".
112147
public let name: String
113148

149+
public init(_ src : PublicKeyCredentialRelyingPartyEntity) {
150+
self.id = src.id
151+
self.name = src.name
152+
}
153+
154+
public init(id : String, name : String) {
155+
self.id = id
156+
self.name = name
157+
}
114158
}
115159

116160
/// From §5.4.3 (https://www.w3.org/TR/webauthn/#dictionary-user-credential-params)
117161
/// The PublicKeyCredentialUserEntity dictionary is used to supply additional user account attributes when
118162
/// creating a new credential.
119163
///
120164
/// When encoding using `Encodable`, `id` is base64url encoded.
121-
public struct PublicKeyCredentialUserEntity: Encodable, Sendable {
165+
public struct PublicKeyCredentialUserEntity: Codable, Sendable {
122166
/// Generated by the Relying Party, unique to the user account, and must not contain personally identifying
123167
/// information about the user.
124168
///
@@ -149,6 +193,15 @@ public struct PublicKeyCredentialUserEntity: Encodable, Sendable {
149193
try container.encode(name, forKey: .name)
150194
try container.encode(displayName, forKey: .displayName)
151195
}
196+
197+
public init(from decoder: any Decoder) throws {
198+
let container = try decoder.container(keyedBy: CodingKeys.self)
199+
let id = try container.decodeBytesFromURLEncodedBase64(forKey: .id)
200+
let name = try container.decode(String.self, forKey: .name)
201+
let displayName = try container.decode(String.self, forKey: .displayName)
202+
self.init(id: id, name: name, displayName: displayName)
203+
}
204+
152205

153206
private enum CodingKeys: String, CodingKey {
154207
case id

Sources/WebAuthn/Ceremonies/Registration/RegistrationCredential.swift

+10-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public struct RegistrationCredential: Sendable {
3131
public let attestationResponse: AuthenticatorAttestationResponse
3232
}
3333

34-
extension RegistrationCredential: Decodable {
34+
extension RegistrationCredential: Codable {
3535
public init(from decoder: Decoder) throws {
3636
let container = try decoder.container(keyedBy: CodingKeys.self)
3737

@@ -47,6 +47,15 @@ extension RegistrationCredential: Decodable {
4747
self.rawID = rawID
4848
attestationResponse = try container.decode(AuthenticatorAttestationResponse.self, forKey: .attestationResponse)
4949
}
50+
51+
public func encode(to encoder: Encoder) throws {
52+
var container = encoder.container(keyedBy: CodingKeys.self)
53+
54+
try container.encode(id, forKey: .id)
55+
try container.encode(rawID.base64URLEncodedString(), forKey: .rawID)
56+
try container.encode(attestationResponse, forKey: .attestationResponse)
57+
}
58+
5059

5160
private enum CodingKeys: String, CodingKey {
5261
case id

Sources/WebAuthn/Ceremonies/Shared/COSE/COSEAlgorithmIdentifier.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import Crypto
1717
/// COSEAlgorithmIdentifier From §5.10.5. A number identifying a cryptographic algorithm. The algorithm
1818
/// identifiers SHOULD be values registered in the IANA COSE Algorithms registry
1919
/// [https://www.w3.org/TR/webauthn/#biblio-iana-cose-algs-reg], for instance, -7 for "ES256" and -257 for "RS256".
20-
public enum COSEAlgorithmIdentifier: Int, RawRepresentable, CaseIterable, Encodable, Sendable {
20+
public enum COSEAlgorithmIdentifier: Int, RawRepresentable, CaseIterable, Codable, Sendable {
2121
/// AlgES256 ECDSA with SHA-256
2222
case algES256 = -7
2323
/// AlgES384 ECDSA with SHA-384

Sources/WebAuthn/Helpers/ChallengeGenerator.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
//
1212
//===----------------------------------------------------------------------===//
1313

14-
package struct ChallengeGenerator: Sendable {
14+
public struct ChallengeGenerator: Sendable {
1515
var generate: @Sendable () -> [UInt8]
1616

1717
package static var live: Self {

0 commit comments

Comments
 (0)