Skip to content

Commit

Permalink
Fix library biases (#28)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
marius-se committed Jul 6, 2023
1 parent 07f9c20 commit a823806
Show file tree
Hide file tree
Showing 29 changed files with 580 additions and 361 deletions.
1 change: 1 addition & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ excluded:
identifier_name:
excluded:
- id
- rp

line_length:
ignores_comments: true
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,40 @@
import Foundation

/// The unprocessed response received from `navigator.credentials.get()`.
public struct AuthenticationCredential: Codable {
///
/// When decoding using `Decodable`, the `rawID` is decoded from base64url to bytes.
public struct AuthenticationCredential {
/// The credential ID of the newly created credential.
public let id: URLEncodedBase64

/// The raw credential ID of the newly created credential.
public let rawID: [UInt8]

/// The attestation response from the authenticator.
public let response: AuthenticatorAssertionResponse

/// Reports the authenticator attachment modality in effect at the time the navigator.credentials.create() or
/// navigator.credentials.get() methods successfully complete
public let authenticatorAttachment: String?

/// Value will always be "public-key" (for now)
public let type: String
}

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

id = try container.decode(URLEncodedBase64.self, forKey: .id)
rawID = try container.decodeBytesFromURLEncodedBase64(forKey: .rawID)
response = try container.decode(AuthenticatorAssertionResponse.self, forKey: .response)
authenticatorAttachment = try container.decodeIfPresent(String.self, forKey: .authenticatorAttachment)
type = try container.decode(String.self, forKey: .type)
}

enum CodingKeys: String, CodingKey {
private enum CodingKeys: String, CodingKey {
case id
case rawID = "rawId"
case response
case authenticatorAttachment
case type
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,57 @@ import Foundation
import Crypto

/// This is what the authenticator device returned after we requested it to authenticate a user.
public struct AuthenticatorAssertionResponse: Codable {
///
/// When decoding using `Decodable`, byte arrays are decoded from base64url to bytes.
public struct AuthenticatorAssertionResponse {
/// Representation of what we passed to `navigator.credentials.get()`
public let clientDataJSON: URLEncodedBase64
///
/// When decoding using `Decodable`, this is decoded from base64url to bytes.
public let clientDataJSON: [UInt8]

/// Contains the authenticator data returned by the authenticator.
public let authenticatorData: URLEncodedBase64
///
/// When decoding using `Decodable`, this is decoded from base64url to bytes.
public let authenticatorData: [UInt8]

/// Contains the raw signature returned from the authenticator
public let signature: URLEncodedBase64
///
/// When decoding using `Decodable`, this is decoded from base64url to bytes.
public let signature: [UInt8]

/// Contains the user handle returned from the authenticator, or null if the authenticator did not return
/// a user handle. Used by to give scope to credentials.
public let userHandle: String?
///
/// When decoding using `Decodable`, this is decoded from base64url to bytes.
public let userHandle: [UInt8]?

/// Contains an attestation object, if the authenticator supports attestation in assertions.
/// The attestation object, if present, includes an attestation statement. Unlike the attestationObject
/// in an AuthenticatorAttestationResponse, it does not contain an authData key because the authenticator
/// data is provided directly in an AuthenticatorAssertionResponse structure.
public let attestationObject: String?
///
/// When decoding using `Decodable`, this is decoded from base64url to bytes.
public let attestationObject: [UInt8]?
}

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

clientDataJSON = try container.decodeBytesFromURLEncodedBase64(forKey: .clientDataJSON)
authenticatorData = try container.decodeBytesFromURLEncodedBase64(forKey: .authenticatorData)
signature = try container.decodeBytesFromURLEncodedBase64(forKey: .signature)
userHandle = try container.decodeBytesFromURLEncodedBase64IfPresent(forKey: .userHandle)
attestationObject = try container.decodeBytesFromURLEncodedBase64IfPresent(forKey: .attestationObject)
}

private enum CodingKeys: String, CodingKey {
case clientDataJSON
case authenticatorData
case signature
case userHandle
case attestationObject
}
}

struct ParsedAuthenticatorAssertionResponse {
Expand All @@ -39,27 +75,21 @@ struct ParsedAuthenticatorAssertionResponse {
let rawAuthenticatorData: Data
let authenticatorData: AuthenticatorData
let signature: URLEncodedBase64
let userHandle: String?
let userHandle: [UInt8]?

init(from authenticatorAssertionResponse: AuthenticatorAssertionResponse) throws {
guard let clientDataData = authenticatorAssertionResponse.clientDataJSON.urlDecoded.decoded else {
throw WebAuthnError.invalidClientDataJSON
}
rawClientData = clientDataData
clientData = try JSONDecoder().decode(CollectedClientData.self, from: clientDataData)
rawClientData = Data(authenticatorAssertionResponse.clientDataJSON)
clientData = try JSONDecoder().decode(CollectedClientData.self, from: rawClientData)

guard let authenticatorDataBytes = authenticatorAssertionResponse.authenticatorData.urlDecoded.decoded else {
throw WebAuthnError.invalidAuthenticatorData
}
rawAuthenticatorData = authenticatorDataBytes
authenticatorData = try AuthenticatorData(bytes: authenticatorDataBytes)
signature = authenticatorAssertionResponse.signature
rawAuthenticatorData = Data(authenticatorAssertionResponse.authenticatorData)
authenticatorData = try AuthenticatorData(bytes: rawAuthenticatorData)
signature = authenticatorAssertionResponse.signature.base64URLEncodedString()
userHandle = authenticatorAssertionResponse.userHandle
}

// swiftlint:disable:next function_parameter_count
func verify(
expectedChallenge: URLEncodedBase64,
expectedChallenge: [UInt8],
relyingPartyOrigin: String,
relyingPartyID: String,
requireUserVerification: Bool,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,27 +15,56 @@
import Foundation

/// The `PublicKeyCredentialRequestOptions` gets passed to the WebAuthn API (`navigator.credentials.get()`)
public struct PublicKeyCredentialRequestOptions: Codable {
///
/// When encoding using `Encodable`, the byte arrays are encoded as base64url.
public struct PublicKeyCredentialRequestOptions: Encodable {
/// A challenge that the authenticator signs, along with other data, when producing an authentication assertion
public let challenge: EncodedBase64
///
/// When encoding using `Encodable` this is encoded as base64url.
public let challenge: [UInt8]

/// The number of milliseconds that the Relying Party is willing to wait for the call to complete. The value is treated
/// as a hint, and may be overridden by the client.
/// See https://www.w3.org/TR/webauthn-2/#dictionary-assertion-options
public let timeout: UInt32?

/// The Relying Party ID.
public let rpId: String?

/// Optionally used by the client to find authenticators eligible for this authentication ceremony.
public let allowCredentials: [PublicKeyCredentialDescriptor]?

/// Specifies whether the user should be verified during the authentication ceremony.
public let userVerification: UserVerificationRequirement?

// let extensions: [String: Any]

public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)

try container.encode(challenge.base64URLEncodedString(), forKey: .challenge)
try container.encodeIfPresent(timeout, forKey: .timeout)
try container.encodeIfPresent(rpId, forKey: .rpId)
try container.encodeIfPresent(allowCredentials, forKey: .allowCredentials)
try container.encodeIfPresent(userVerification, forKey: .userVerification)
}

private enum CodingKeys: String, CodingKey {
case challenge
case timeout
case rpId
case allowCredentials
case userVerification
}
}

/// Information about a generated credential.
public struct PublicKeyCredentialDescriptor: Codable, Equatable {
///
/// When encoding using `Encodable`, `id` is encoded as base64url.
public struct PublicKeyCredentialDescriptor: Equatable, Encodable {
/// 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, Codable, Equatable {
public enum AuthenticatorTransport: String, Equatable, Encodable {
/// 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 All @@ -51,14 +80,14 @@ public struct PublicKeyCredentialDescriptor: Codable, Equatable {
case `internal`
}

enum CodingKeys: String, CodingKey {
case type, id, transports
}

/// Will always be 'public-key'
public let type: String

/// The sequence of bytes representing the credential's ID
///
/// When encoding using `Encodable`, this is encoded as base64url.
public let id: [UInt8]

/// The types of connections to the client/browser the authenticator supports
public let transports: [AuthenticatorTransport]

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

try container.encode(type, forKey: .type)
try container.encode(id.base64EncodedString(), forKey: .id)
try container.encode(transports, forKey: .transports)
try container.encode(id.base64URLEncodedString(), forKey: .id)
try container.encodeIfPresent(transports, forKey: .transports)
}

private enum CodingKeys: String, CodingKey {
case type
case id
case transports
}
}

/// 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, Codable {
public enum UserVerificationRequirement: String, Encodable {
/// 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 @@ -16,7 +16,7 @@ import Foundation

/// On successful authentication, this structure contains a summary of the authentication flow
public struct VerifiedAuthentication {
public enum CredentialDeviceType: String, Codable {
public enum CredentialDeviceType: String {
case singleDevice = "single_device"
case multiDevice = "multi_device"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
/// Options to specify the Relying Party's preference regarding attestation conveyance during credential generation.
///
/// Currently only supports `none`.
public enum AttestationConveyancePreference: String, Codable {
public enum AttestationConveyancePreference: String, Encodable {
/// 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 @@ -16,9 +16,32 @@ import Foundation
import SwiftCBOR

/// The response from the authenticator device for the creation of a new public key credential.
public struct AuthenticatorAttestationResponse: Codable {
public let clientDataJSON: URLEncodedBase64
public let attestationObject: URLEncodedBase64
///
/// When decoding using `Decodable`, `clientDataJSON` and `attestationObject` are decoded from base64url to bytes.
public struct AuthenticatorAttestationResponse {
/// The client data that was passed to the authenticator during the creation ceremony.
///
/// When decoding using `Decodable`, this is decoded from base64url to bytes.
public let clientDataJSON: [UInt8]

/// Contains both attestation data and attestation statement.
///
/// When decoding using `Decodable`, this is decoded from base64url to bytes.
public let attestationObject: [UInt8]
}

extension AuthenticatorAttestationResponse: Decodable {
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)
}

private enum CodingKeys: String, CodingKey {
case clientDataJSON
case attestationObject
}
}

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

init(from rawResponse: AuthenticatorAttestationResponse) throws {
// assembling clientData
guard let clientDataJSONData = rawResponse.clientDataJSON.urlDecoded.decoded else {
throw WebAuthnError.invalidClientDataJSON
}
let clientData = try JSONDecoder().decode(CollectedClientData.self, from: clientDataJSONData)
let clientData = try JSONDecoder().decode(CollectedClientData.self, from: Data(rawResponse.clientDataJSON))
self.clientData = clientData

// Step 11. (assembling attestationObject)
guard let attestationObjectData = rawResponse.attestationObject.urlDecoded.decoded,
let decodedAttestationObject = try CBOR.decode([UInt8](attestationObjectData)) else {
let attestationObjectData = Data(rawResponse.attestationObject)
guard let decodedAttestationObject = try? CBOR.decode([UInt8](attestationObjectData)) else {
throw WebAuthnError.invalidAttestationObject
}

Expand Down
Loading

0 comments on commit a823806

Please sign in to comment.