Skip to content

Commit a823806

Browse files
authored
Fix library biases (#28)
* change user id and challenge type to byte array * rename a few properties * update createRegistrationOptions documentation * make PublicKeyCredentialUserEntity a struct * make storedChallenge byte array instead of base64 encoded * fix CollectedClientData challenge type * fix CollectedClientData challenge type * add getChallenge to RegistrationCredential * remove Codable conformances * wip * wip * wip * conform PublicKeyCredentialRequestOptions to Encodable * wip * fix config documentation comment * revert renaming beginRegistration * update documentation comments * fix tests
1 parent 07f9c20 commit a823806

29 files changed

+580
-361
lines changed

.swiftlint.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ excluded:
77
identifier_name:
88
excluded:
99
- id
10+
- rp
1011

1112
line_length:
1213
ignores_comments: true

Sources/WebAuthn/Ceremonies/Authentication/AuthenticationCredential.swift

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,40 @@
1515
import Foundation
1616

1717
/// The unprocessed response received from `navigator.credentials.get()`.
18-
public struct AuthenticationCredential: Codable {
18+
///
19+
/// When decoding using `Decodable`, the `rawID` is decoded from base64url to bytes.
20+
public struct AuthenticationCredential {
21+
/// The credential ID of the newly created credential.
1922
public let id: URLEncodedBase64
23+
24+
/// The raw credential ID of the newly created credential.
25+
public let rawID: [UInt8]
26+
27+
/// The attestation response from the authenticator.
2028
public let response: AuthenticatorAssertionResponse
29+
30+
/// Reports the authenticator attachment modality in effect at the time the navigator.credentials.create() or
31+
/// navigator.credentials.get() methods successfully complete
2132
public let authenticatorAttachment: String?
33+
34+
/// Value will always be "public-key" (for now)
2235
public let type: String
36+
}
37+
38+
extension AuthenticationCredential: Decodable {
39+
public init(from decoder: Decoder) throws {
40+
let container = try decoder.container(keyedBy: CodingKeys.self)
41+
42+
id = try container.decode(URLEncodedBase64.self, forKey: .id)
43+
rawID = try container.decodeBytesFromURLEncodedBase64(forKey: .rawID)
44+
response = try container.decode(AuthenticatorAssertionResponse.self, forKey: .response)
45+
authenticatorAttachment = try container.decodeIfPresent(String.self, forKey: .authenticatorAttachment)
46+
type = try container.decode(String.self, forKey: .type)
47+
}
2348

24-
enum CodingKeys: String, CodingKey {
49+
private enum CodingKeys: String, CodingKey {
2550
case id
51+
case rawID = "rawId"
2652
case response
2753
case authenticatorAttachment
2854
case type

Sources/WebAuthn/Ceremonies/Authentication/AuthenticatorAssertionResponse.swift

Lines changed: 49 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,57 @@ import Foundation
1616
import Crypto
1717

1818
/// This is what the authenticator device returned after we requested it to authenticate a user.
19-
public struct AuthenticatorAssertionResponse: Codable {
19+
///
20+
/// When decoding using `Decodable`, byte arrays are decoded from base64url to bytes.
21+
public struct AuthenticatorAssertionResponse {
2022
/// Representation of what we passed to `navigator.credentials.get()`
21-
public let clientDataJSON: URLEncodedBase64
23+
///
24+
/// When decoding using `Decodable`, this is decoded from base64url to bytes.
25+
public let clientDataJSON: [UInt8]
26+
2227
/// Contains the authenticator data returned by the authenticator.
23-
public let authenticatorData: URLEncodedBase64
28+
///
29+
/// When decoding using `Decodable`, this is decoded from base64url to bytes.
30+
public let authenticatorData: [UInt8]
31+
2432
/// Contains the raw signature returned from the authenticator
25-
public let signature: URLEncodedBase64
33+
///
34+
/// When decoding using `Decodable`, this is decoded from base64url to bytes.
35+
public let signature: [UInt8]
36+
2637
/// Contains the user handle returned from the authenticator, or null if the authenticator did not return
2738
/// a user handle. Used by to give scope to credentials.
28-
public let userHandle: String?
39+
///
40+
/// When decoding using `Decodable`, this is decoded from base64url to bytes.
41+
public let userHandle: [UInt8]?
42+
2943
/// Contains an attestation object, if the authenticator supports attestation in assertions.
3044
/// The attestation object, if present, includes an attestation statement. Unlike the attestationObject
3145
/// in an AuthenticatorAttestationResponse, it does not contain an authData key because the authenticator
3246
/// data is provided directly in an AuthenticatorAssertionResponse structure.
33-
public let attestationObject: String?
47+
///
48+
/// When decoding using `Decodable`, this is decoded from base64url to bytes.
49+
public let attestationObject: [UInt8]?
50+
}
51+
52+
extension AuthenticatorAssertionResponse: Decodable {
53+
public init(from decoder: Decoder) throws {
54+
let container = try decoder.container(keyedBy: CodingKeys.self)
55+
56+
clientDataJSON = try container.decodeBytesFromURLEncodedBase64(forKey: .clientDataJSON)
57+
authenticatorData = try container.decodeBytesFromURLEncodedBase64(forKey: .authenticatorData)
58+
signature = try container.decodeBytesFromURLEncodedBase64(forKey: .signature)
59+
userHandle = try container.decodeBytesFromURLEncodedBase64IfPresent(forKey: .userHandle)
60+
attestationObject = try container.decodeBytesFromURLEncodedBase64IfPresent(forKey: .attestationObject)
61+
}
62+
63+
private enum CodingKeys: String, CodingKey {
64+
case clientDataJSON
65+
case authenticatorData
66+
case signature
67+
case userHandle
68+
case attestationObject
69+
}
3470
}
3571

3672
struct ParsedAuthenticatorAssertionResponse {
@@ -39,27 +75,21 @@ struct ParsedAuthenticatorAssertionResponse {
3975
let rawAuthenticatorData: Data
4076
let authenticatorData: AuthenticatorData
4177
let signature: URLEncodedBase64
42-
let userHandle: String?
78+
let userHandle: [UInt8]?
4379

4480
init(from authenticatorAssertionResponse: AuthenticatorAssertionResponse) throws {
45-
guard let clientDataData = authenticatorAssertionResponse.clientDataJSON.urlDecoded.decoded else {
46-
throw WebAuthnError.invalidClientDataJSON
47-
}
48-
rawClientData = clientDataData
49-
clientData = try JSONDecoder().decode(CollectedClientData.self, from: clientDataData)
81+
rawClientData = Data(authenticatorAssertionResponse.clientDataJSON)
82+
clientData = try JSONDecoder().decode(CollectedClientData.self, from: rawClientData)
5083

51-
guard let authenticatorDataBytes = authenticatorAssertionResponse.authenticatorData.urlDecoded.decoded else {
52-
throw WebAuthnError.invalidAuthenticatorData
53-
}
54-
rawAuthenticatorData = authenticatorDataBytes
55-
authenticatorData = try AuthenticatorData(bytes: authenticatorDataBytes)
56-
signature = authenticatorAssertionResponse.signature
84+
rawAuthenticatorData = Data(authenticatorAssertionResponse.authenticatorData)
85+
authenticatorData = try AuthenticatorData(bytes: rawAuthenticatorData)
86+
signature = authenticatorAssertionResponse.signature.base64URLEncodedString()
5787
userHandle = authenticatorAssertionResponse.userHandle
5888
}
5989

6090
// swiftlint:disable:next function_parameter_count
6191
func verify(
62-
expectedChallenge: URLEncodedBase64,
92+
expectedChallenge: [UInt8],
6393
relyingPartyOrigin: String,
6494
relyingPartyID: String,
6595
requireUserVerification: Bool,

Sources/WebAuthn/Ceremonies/Authentication/PublicKeyCredentialRequestOptions.swift

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,27 +15,56 @@
1515
import Foundation
1616

1717
/// The `PublicKeyCredentialRequestOptions` gets passed to the WebAuthn API (`navigator.credentials.get()`)
18-
public struct PublicKeyCredentialRequestOptions: Codable {
18+
///
19+
/// When encoding using `Encodable`, the byte arrays are encoded as base64url.
20+
public struct PublicKeyCredentialRequestOptions: Encodable {
1921
/// A challenge that the authenticator signs, along with other data, when producing an authentication assertion
20-
public let challenge: EncodedBase64
22+
///
23+
/// When encoding using `Encodable` this is encoded as base64url.
24+
public let challenge: [UInt8]
25+
2126
/// The number of milliseconds that the Relying Party is willing to wait for the call to complete. The value is treated
2227
/// as a hint, and may be overridden by the client.
2328
/// See https://www.w3.org/TR/webauthn-2/#dictionary-assertion-options
2429
public let timeout: UInt32?
30+
2531
/// The Relying Party ID.
2632
public let rpId: String?
33+
2734
/// Optionally used by the client to find authenticators eligible for this authentication ceremony.
2835
public let allowCredentials: [PublicKeyCredentialDescriptor]?
36+
2937
/// Specifies whether the user should be verified during the authentication ceremony.
3038
public let userVerification: UserVerificationRequirement?
39+
3140
// let extensions: [String: Any]
41+
42+
public func encode(to encoder: Encoder) throws {
43+
var container = encoder.container(keyedBy: CodingKeys.self)
44+
45+
try container.encode(challenge.base64URLEncodedString(), forKey: .challenge)
46+
try container.encodeIfPresent(timeout, forKey: .timeout)
47+
try container.encodeIfPresent(rpId, forKey: .rpId)
48+
try container.encodeIfPresent(allowCredentials, forKey: .allowCredentials)
49+
try container.encodeIfPresent(userVerification, forKey: .userVerification)
50+
}
51+
52+
private enum CodingKeys: String, CodingKey {
53+
case challenge
54+
case timeout
55+
case rpId
56+
case allowCredentials
57+
case userVerification
58+
}
3259
}
3360

3461
/// Information about a generated credential.
35-
public struct PublicKeyCredentialDescriptor: Codable, Equatable {
62+
///
63+
/// When encoding using `Encodable`, `id` is encoded as base64url.
64+
public struct PublicKeyCredentialDescriptor: Equatable, Encodable {
3665
/// Defines hints as to how clients might communicate with a particular authenticator in order to obtain an
3766
/// assertion for a specific credential
38-
public enum AuthenticatorTransport: String, Codable, Equatable {
67+
public enum AuthenticatorTransport: String, Equatable, Encodable {
3968
/// Indicates the respective authenticator can be contacted over removable USB.
4069
case usb
4170
/// Indicates the respective authenticator can be contacted over Near Field Communication (NFC).
@@ -51,14 +80,14 @@ public struct PublicKeyCredentialDescriptor: Codable, Equatable {
5180
case `internal`
5281
}
5382

54-
enum CodingKeys: String, CodingKey {
55-
case type, id, transports
56-
}
57-
5883
/// Will always be 'public-key'
5984
public let type: String
85+
6086
/// The sequence of bytes representing the credential's ID
87+
///
88+
/// When encoding using `Encodable`, this is encoded as base64url.
6189
public let id: [UInt8]
90+
6291
/// The types of connections to the client/browser the authenticator supports
6392
public let transports: [AuthenticatorTransport]
6493

@@ -72,14 +101,20 @@ public struct PublicKeyCredentialDescriptor: Codable, Equatable {
72101
var container = encoder.container(keyedBy: CodingKeys.self)
73102

74103
try container.encode(type, forKey: .type)
75-
try container.encode(id.base64EncodedString(), forKey: .id)
76-
try container.encode(transports, forKey: .transports)
104+
try container.encode(id.base64URLEncodedString(), forKey: .id)
105+
try container.encodeIfPresent(transports, forKey: .transports)
106+
}
107+
108+
private enum CodingKeys: String, CodingKey {
109+
case type
110+
case id
111+
case transports
77112
}
78113
}
79114

80115
/// The Relying Party may require user verification for some of its operations but not for others, and may use this
81116
/// type to express its needs.
82-
public enum UserVerificationRequirement: String, Codable {
117+
public enum UserVerificationRequirement: String, Encodable {
83118
/// The Relying Party requires user verification for the operation and will fail the overall ceremony if the
84119
/// user wasn't verified.
85120
case required

Sources/WebAuthn/Ceremonies/Authentication/VerifiedAuthentication.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import Foundation
1616

1717
/// On successful authentication, this structure contains a summary of the authentication flow
1818
public struct VerifiedAuthentication {
19-
public enum CredentialDeviceType: String, Codable {
19+
public enum CredentialDeviceType: String {
2020
case singleDevice = "single_device"
2121
case multiDevice = "multi_device"
2222
}

Sources/WebAuthn/Ceremonies/Registration/AttestationConveyancePreference.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
/// Options to specify the Relying Party's preference regarding attestation conveyance during credential generation.
1616
///
1717
/// Currently only supports `none`.
18-
public enum AttestationConveyancePreference: String, Codable {
18+
public enum AttestationConveyancePreference: String, Encodable {
1919
/// Indicates the Relying Party is not interested in authenticator attestation.
2020
case none
2121
// case indirect

Sources/WebAuthn/Ceremonies/Registration/AuthenticatorAttestationResponse.swift

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,32 @@ import Foundation
1616
import SwiftCBOR
1717

1818
/// The response from the authenticator device for the creation of a new public key credential.
19-
public struct AuthenticatorAttestationResponse: Codable {
20-
public let clientDataJSON: URLEncodedBase64
21-
public let attestationObject: URLEncodedBase64
19+
///
20+
/// When decoding using `Decodable`, `clientDataJSON` and `attestationObject` are decoded from base64url to bytes.
21+
public struct AuthenticatorAttestationResponse {
22+
/// The client data that was passed to the authenticator during the creation ceremony.
23+
///
24+
/// When decoding using `Decodable`, this is decoded from base64url to bytes.
25+
public let clientDataJSON: [UInt8]
26+
27+
/// Contains both attestation data and attestation statement.
28+
///
29+
/// When decoding using `Decodable`, this is decoded from base64url to bytes.
30+
public let attestationObject: [UInt8]
31+
}
32+
33+
extension AuthenticatorAttestationResponse: Decodable {
34+
public init(from decoder: Decoder) throws {
35+
let container = try decoder.container(keyedBy: CodingKeys.self)
36+
37+
clientDataJSON = try container.decodeBytesFromURLEncodedBase64(forKey: .clientDataJSON)
38+
attestationObject = try container.decodeBytesFromURLEncodedBase64(forKey: .attestationObject)
39+
}
40+
41+
private enum CodingKeys: String, CodingKey {
42+
case clientDataJSON
43+
case attestationObject
44+
}
2245
}
2346

2447
/// A parsed version of `AuthenticatorAttestationResponse`
@@ -28,15 +51,12 @@ struct ParsedAuthenticatorAttestationResponse {
2851

2952
init(from rawResponse: AuthenticatorAttestationResponse) throws {
3053
// assembling clientData
31-
guard let clientDataJSONData = rawResponse.clientDataJSON.urlDecoded.decoded else {
32-
throw WebAuthnError.invalidClientDataJSON
33-
}
34-
let clientData = try JSONDecoder().decode(CollectedClientData.self, from: clientDataJSONData)
54+
let clientData = try JSONDecoder().decode(CollectedClientData.self, from: Data(rawResponse.clientDataJSON))
3555
self.clientData = clientData
3656

3757
// Step 11. (assembling attestationObject)
38-
guard let attestationObjectData = rawResponse.attestationObject.urlDecoded.decoded,
39-
let decodedAttestationObject = try CBOR.decode([UInt8](attestationObjectData)) else {
58+
let attestationObjectData = Data(rawResponse.attestationObject)
59+
guard let decodedAttestationObject = try? CBOR.decode([UInt8](attestationObjectData)) else {
4060
throw WebAuthnError.invalidAttestationObject
4161
}
4262

0 commit comments

Comments
 (0)