Skip to content

Commit 252b4c7

Browse files
committed
further simplify beginRegistration
1 parent 402e06f commit 252b4c7

9 files changed

+110
-64
lines changed

Sources/WebAuthn/Helpers/WebAuthn+generateChallenge.swift renamed to Sources/WebAuthn/Helpers/Base64Utilities.swift

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,6 @@
11
import Foundation
22
import Logging
33

4-
extension WebAuthnManager {
5-
struct ChallengeGeneratorError: Error {}
6-
/// Generate a suitably random value to be used as an attestation or assertion challenge
7-
/// - Throws: An error if something went wrong while generating random byte
8-
/// - Returns: 32 bytes
9-
public static func generateChallenge() throws -> [UInt8] {
10-
var bytes = [UInt8](repeating: 0, count: 32)
11-
let status = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes)
12-
guard status == errSecSuccess else { throw ChallengeGeneratorError() }
13-
return bytes
14-
}
15-
}
16-
174
extension Array where Element == UInt8 {
185
/// Encodes an array of bytes into a base64url-encoded string
196
/// - Returns: A base64url-encoded string
@@ -37,4 +24,14 @@ extension String {
3724
public static func base64(fromBase64URLEncoded base64URLEncoded: String) -> Self {
3825
return base64URLEncoded.replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/")
3926
}
27+
}
28+
29+
extension String {
30+
var base64URLDecodedData: Data? {
31+
var result = self.replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/")
32+
while result.count % 4 != 0 {
33+
result = result.appending("=")
34+
}
35+
return Data(base64Encoded: result)
36+
}
4037
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
1+
import Foundation
2+
13
/// See §5.4. https://www.w3.org/TR/webauthn/#dictionary-makecredentialoptions
24
/// Contains a PublicKeyCredentialCreationOptions object specifying the desired attributes of the
35
/// to-be-created public key credential.
46
public struct PublicKeyCredentialCreationOptions: Codable {
7+
/// Base64-encoded challenge string
58
public let challenge: String
69
public let user: PublicKeyCredentialUserEntity
710
public let relyingParty: PublicKeyCredentialRpEntity
11+
public let publicKeyCredentialParameters: [PublicKeyCredentialParameters]
12+
public let timeout: TimeInterval
813
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
public enum PublicKeyCredentialParameters: Codable, CaseIterable {
2+
case algES256
3+
// TODO: Add more algorithms
4+
5+
enum CodingKeys: String, CodingKey {
6+
case alg
7+
case type
8+
}
9+
10+
public init(from decoder: Decoder) throws {
11+
let container = try decoder.container(keyedBy: CodingKeys.self)
12+
guard try container.decode(String.self, forKey: .type) == "public-key" else {
13+
throw DecodingError.dataCorruptedError(
14+
forKey: .type,
15+
in: container,
16+
debugDescription: "Currently only public-key is supported."
17+
)
18+
}
19+
20+
let algorithm = try container.decode(String.self, forKey: .alg)
21+
switch algorithm {
22+
case "AlgES256": self = .algES256
23+
default:
24+
throw DecodingError.dataCorruptedError(
25+
forKey: .alg,
26+
in: container,
27+
debugDescription: "Unsupported algorithm: \(algorithm)"
28+
)
29+
}
30+
}
31+
32+
public func encode(to encoder: Encoder) throws {
33+
var container = encoder.container(keyedBy: CodingKeys.self)
34+
35+
try container.encode("public-key", forKey: .type)
36+
37+
switch self {
38+
case .algES256: try container.encode("AlgES256", forKey: .alg)
39+
}
40+
}
41+
}

Sources/WebAuthn/SessionData.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
/// SessionData is the data that should be stored by the Relying Party for the duration of the web authentication
22
/// ceremony
33
public struct SessionData {
4+
/// Base64url-encoded challenge string
45
public let challenge: String
6+
/// Plain user id (not encoded)
57
public let userID: String
68
}

Sources/WebAuthn/String+Base64URL.swift

Lines changed: 0 additions & 25 deletions
This file was deleted.

Sources/WebAuthn/User.swift

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
1-
public struct User {
2-
public let id: String
3-
public let name: String
4-
public let displayName: String
5-
6-
public init(id: String, name: String, displayName: String) {
7-
self.id = id
8-
self.name = name
9-
self.displayName = displayName
10-
}
1+
/// Protocol to interact with a user throughout the registration ceremony
2+
public protocol User {
3+
var userID: String { get }
4+
var name: String { get }
5+
var displayName: String { get }
116
}

Sources/WebAuthn/WebAuthnConfig.swift

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
public struct Config {
1+
import Foundation
2+
3+
public struct WebAuthnConfig {
24
public let relyingPartyDisplayName: String
35
public let relyingPartyID: String
6+
public let timeout: TimeInterval
47

5-
public init(relyingPartyDisplayName: String, relyingPartyID: String) {
8+
public init(relyingPartyDisplayName: String, relyingPartyID: String, timeout: TimeInterval) {
69
self.relyingPartyDisplayName = relyingPartyDisplayName
710
self.relyingPartyID = relyingPartyID
11+
self.timeout = timeout
812
}
9-
}
10-
11-
public var config: Config!
13+
}

Sources/WebAuthn/WebAuthnManager.swift

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,36 @@ import Crypto
1717
import Logging
1818
import Foundation
1919

20-
public enum WebAuthnManager {
20+
public struct WebAuthnManager {
21+
enum WebAuthnManagerError: Error {
22+
case base64EncodingFailed
23+
case challengeGenerationFailed
24+
}
25+
private let config: WebAuthnConfig
26+
27+
public init(config: WebAuthnConfig) {
28+
self.config = config
29+
}
30+
2131
/// Generate a new set of registration data to be sent to the client and authenticator.
22-
public static func beginRegistration(user: User) throws -> (PublicKeyCredentialCreationOptions, SessionData) {
23-
let userEntity = PublicKeyCredentialUserEntity(name: user.name, id: user.id, displayName: user.displayName)
32+
public func beginRegistration(user: User) throws -> (PublicKeyCredentialCreationOptions, SessionData) {
33+
guard let base64ID = user.userID.data(using: .utf8)?.base64EncodedString() else {
34+
throw WebAuthnManagerError.base64EncodingFailed
35+
}
36+
37+
let userEntity = PublicKeyCredentialUserEntity(name: user.name, id: base64ID, displayName: user.displayName)
2438
let relyingParty = PublicKeyCredentialRpEntity(name: config.relyingPartyDisplayName, id: config.relyingPartyID)
2539

2640
let challenge = try generateChallenge()
2741

2842
let options = PublicKeyCredentialCreationOptions(
2943
challenge: challenge.base64EncodedString(),
3044
user: userEntity,
31-
relyingParty: relyingParty
45+
relyingParty: relyingParty,
46+
publicKeyCredentialParameters: PublicKeyCredentialParameters.allCases,
47+
timeout: config.timeout
3248
)
33-
let sessionData = SessionData(challenge: challenge.base64URLEncodedString(), userID: user.id)
49+
let sessionData = SessionData(challenge: challenge.base64URLEncodedString(), userID: user.userID)
3450

3551
return (options, sessionData)
3652
}
@@ -44,7 +60,7 @@ public enum WebAuthnManager {
4460
/// - logger: A logger
4561
/// - Throws:
4662
/// - An error if the authentication response isn't valid
47-
public static func verifyAuthenticationResponse(
63+
public func verifyAuthenticationResponse(
4864
_ data: AuthenticationResponse,
4965
expectedChallenge: String,
5066
publicKey: P256.Signing.PublicKey,
@@ -74,7 +90,7 @@ public enum WebAuthnManager {
7490
}
7591
}
7692

77-
public static func parseRegisterCredentials(
93+
public func parseRegisterCredentials(
7894
_ data: RegistrationResponse,
7995
challengeProvided: String,
8096
origin: String,
@@ -148,7 +164,7 @@ public enum WebAuthnManager {
148164
return Credential(credentialID: data.id, publicKey: key)
149165
}
150166

151-
static func parseAttestedData(
167+
func parseAttestedData(
152168
_ data: [UInt8],
153169
logger: Logger
154170
) throws -> AttestedCredentialData {
@@ -168,7 +184,7 @@ public enum WebAuthnManager {
168184
return AttestedCredentialData(aaguid: Array(aaguid), credentialID: Array(credentialID), publicKey: Array(publicKeyBytes))
169185
}
170186

171-
static func parseAttestationObject(
187+
func parseAttestationObject(
172188
_ bytes: [UInt8],
173189
logger: Logger
174190
) throws -> AttestedCredentialData? {
@@ -219,4 +235,16 @@ public enum WebAuthnManager {
219235
}
220236
return credentialsData
221237
}
238+
}
239+
240+
extension WebAuthnManager {
241+
/// Generate a suitably random value to be used as an attestation or assertion challenge
242+
/// - Throws: An error if something went wrong while generating random byte
243+
/// - Returns: 32 bytes
244+
func generateChallenge() throws -> [UInt8] {
245+
var bytes = [UInt8](repeating: 0, count: 32)
246+
let status = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes)
247+
guard status == errSecSuccess else { throw WebAuthnManagerError.challengeGenerationFailed }
248+
return bytes
249+
}
222250
}

Tests/WebAuthnTests/HelpersTests.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ import XCTest
33

44
final class HelpersTests: XCTestCase {
55
func testGenerateChallengeReturnsRandomBytes() throws {
6-
let challenge1 = try WebAuthnManager.generateChallenge()
7-
let challenge2 = try WebAuthnManager.generateChallenge()
6+
let webAuthn = WebAuthnManager(config: .init(relyingPartyDisplayName: "123", relyingPartyID: "1", timeout: 60))
7+
let challenge1 = try webAuthn.generateChallenge()
8+
let challenge2 = try webAuthn.generateChallenge()
89

910
XCTAssertNotEqual(challenge1, challenge2)
1011
}

0 commit comments

Comments
 (0)