Skip to content

Commit 1ce64d6

Browse files
authored
Merge pull request #20 from swift-server/base64types
Make EncodedBase64 and URLEncodedBase64 distinct types
2 parents 07359c8 + 98395c5 commit 1ce64d6

File tree

7 files changed

+126
-55
lines changed

7 files changed

+126
-55
lines changed

Sources/WebAuthn/Ceremonies/Authentication/PublicKeyCredentialRequestOptions.swift

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

1717
/// The `PublicKeyCredentialRequestOptions` gets passed to the WebAuthn API (`navigator.credentials.get()`)
1818
public struct PublicKeyCredentialRequestOptions: Codable {
19-
public let challenge: String
19+
public let challenge: EncodedBase64
2020
public let timeout: TimeInterval?
2121
public let rpId: String?
2222
public let allowCredentials: [PublicKeyCredentialDescriptor]?

Sources/WebAuthn/Ceremonies/Registration/AuthenticatorAttestationResponse.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,14 @@ struct ParsedAuthenticatorAttestationResponse {
2828

2929
init(from rawResponse: AuthenticatorAttestationResponse) throws {
3030
// assembling clientData
31-
guard let clientDataJSONData = rawResponse.clientDataJSON.base64URLDecodedData else {
31+
guard let clientDataJSONData = rawResponse.clientDataJSON.urlDecoded.decoded else {
3232
throw WebAuthnError.invalidClientDataJSON
3333
}
3434
let clientData = try JSONDecoder().decode(CollectedClientData.self, from: clientDataJSONData)
3535
self.clientData = clientData
3636

3737
// Step 11. (assembling attestationObject)
38-
guard let attestationObjectData = rawResponse.attestationObject.base64URLDecodedData,
38+
guard let attestationObjectData = rawResponse.attestationObject.urlDecoded.decoded,
3939
let decodedAttestationObject = try CBOR.decode([UInt8](attestationObjectData)) else {
4040
throw WebAuthnError.invalidAttestationObject
4141
}

Sources/WebAuthn/Ceremonies/Registration/RegistrationCredential.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ struct ParsedCredentialCreationResponse {
4444
init(from rawResponse: RegistrationCredential) throws {
4545
id = rawResponse.id
4646

47-
guard let decodedRawID = rawResponse.rawID.base64URLDecodedData else {
47+
guard let decodedRawID = rawResponse.rawID.urlDecoded.decoded else {
4848
throw WebAuthnError.invalidRawID
4949
}
5050
rawID = decodedRawID
@@ -72,7 +72,7 @@ struct ParsedCredentialCreationResponse {
7272
)
7373

7474
// Step 10.
75-
guard let clientData = raw.clientDataJSON.data(using: .utf8) else {
75+
guard let clientData = raw.clientDataJSON.asData() else {
7676
throw WebAuthnError.invalidClientDataJSON
7777
}
7878
let hash = SHA256.hash(data: clientData)

Sources/WebAuthn/Helpers/Base64Utilities.swift

Lines changed: 95 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -15,56 +15,120 @@
1515
import Foundation
1616
import Logging
1717

18-
public typealias URLEncodedBase64 = String
19-
public typealias EncodedBase64 = String
18+
/// Container for base64 encoded data
19+
public struct EncodedBase64: ExpressibleByStringLiteral, Codable, Hashable, Equatable {
20+
private let base64: String
21+
22+
public init(_ string: String) {
23+
self.base64 = string
24+
}
25+
26+
public init(stringLiteral value: StringLiteralType) {
27+
self.init(value)
28+
}
29+
30+
public init(from decoder: Decoder) throws {
31+
let container = try decoder.singleValueContainer()
32+
self.base64 = try container.decode(String.self)
33+
}
34+
35+
public func encode(to encoder: Encoder) throws {
36+
var container = encoder.singleValueContainer()
37+
try container.encode(self.base64)
38+
}
39+
40+
/// Return as URL encoded base64
41+
public var urlEncoded: URLEncodedBase64 {
42+
return .init(
43+
self.base64.replacingOccurrences(of: "+", with: "-")
44+
.replacingOccurrences(of: "/", with: "_")
45+
.replacingOccurrences(of: "=", with: "")
46+
)
47+
}
48+
49+
/// Return base64 decoded data
50+
public var decoded: Data? {
51+
return Data(base64Encoded: self.base64)
52+
}
53+
54+
/// return Base64 data as a String
55+
public func asString() -> String {
56+
return self.base64
57+
}
58+
59+
/// return Base64 data as Data
60+
public func asData() -> Data? {
61+
return self.base64.data(using: .utf8)
62+
}
63+
}
64+
65+
/// Container for URL encoded base64 data
66+
public struct URLEncodedBase64: ExpressibleByStringLiteral, Codable, Hashable, Equatable {
67+
let base64: String
68+
69+
public init(_ string: String) {
70+
self.base64 = string
71+
}
72+
73+
public init(stringLiteral value: StringLiteralType) {
74+
self.init(value)
75+
}
76+
77+
public init(from decoder: Decoder) throws {
78+
let container = try decoder.singleValueContainer()
79+
self.base64 = try container.decode(String.self)
80+
}
81+
82+
public func encode(to encoder: Encoder) throws {
83+
var container = encoder.singleValueContainer()
84+
try container.encode(self.base64)
85+
}
86+
87+
/// Return URL decoded Base64 data
88+
public var urlDecoded: EncodedBase64 {
89+
var result = self.base64.replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/")
90+
while result.count % 4 != 0 {
91+
result = result.appending("=")
92+
}
93+
return .init(result)
94+
}
95+
96+
/// return Base64 data as a String
97+
public func asString() -> String {
98+
return self.base64
99+
}
100+
101+
/// return Base64 data as Data
102+
public func asData() -> Data? {
103+
return self.base64.data(using: .utf8)
104+
}
105+
}
20106

21107
extension Array where Element == UInt8 {
22108
/// Encodes an array of bytes into a base64url-encoded string
23109
/// - Returns: A base64url-encoded string
24-
public func base64URLEncodedString() -> String {
110+
public func base64URLEncodedString() -> URLEncodedBase64 {
25111
let base64String = Data(bytes: self, count: self.count).base64EncodedString()
26-
return String.base64URL(fromBase64: base64String)
112+
return EncodedBase64(base64String).urlEncoded
27113
}
28114

29115
/// Encodes an array of bytes into a base64 string
30116
/// - Returns: A base64-encoded string
31-
public func base64EncodedString() -> String {
32-
return Data(bytes: self, count: self.count).base64EncodedString()
117+
public func base64EncodedString() -> EncodedBase64 {
118+
return .init(Data(bytes: self, count: self.count).base64EncodedString())
33119
}
34120
}
35121

36122
extension Data {
37123
/// Encodes data into a base64url-encoded string
38124
/// - Returns: A base64url-encoded string
39-
public func base64URLEncodedString() -> String {
125+
public func base64URLEncodedString() -> URLEncodedBase64 {
40126
return [UInt8](self).base64URLEncodedString()
41127
}
42128
}
43129

44130
extension String {
45-
/// Decode a base64url-encoded `String` to a base64 `String`
46-
/// - Returns: A base64-encoded `String`
47-
public static func base64(fromBase64URLEncoded base64URLEncoded: String) -> Self {
48-
return base64URLEncoded.replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/")
49-
}
50-
51-
public static func base64URL(fromBase64 base64Encoded: String) -> Self {
52-
return base64Encoded.replacingOccurrences(of: "+", with: "-")
53-
.replacingOccurrences(of: "/", with: "_")
54-
.replacingOccurrences(of: "=", with: "")
55-
}
56-
57-
func toBase64() -> String {
58-
return Data(self.utf8).base64EncodedString()
59-
}
60-
}
61-
62-
extension String {
63-
public var base64URLDecodedData: Data? {
64-
var result = self.replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/")
65-
while result.count % 4 != 0 {
66-
result = result.appending("=")
67-
}
68-
return Data(base64Encoded: result)
131+
func toBase64() -> EncodedBase64 {
132+
return .init(Data(self.utf8).base64EncodedString())
69133
}
70134
}

Sources/WebAuthn/WebAuthnManager.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ public struct WebAuthnManager {
6767
// Step 3. - 16.
6868
let parsedData = try ParsedCredentialCreationResponse(from: credentialCreationData)
6969
try parsedData.verify(
70-
storedChallenge: String.base64URL(fromBase64: challenge),
70+
storedChallenge: challenge.urlEncoded,
7171
verifyUser: requireUserVerification,
7272
relyingPartyID: config.relyingPartyID,
7373
relyingPartyOrigin: config.relyingPartyOrigin
@@ -104,7 +104,7 @@ public struct WebAuthnManager {
104104
}
105105

106106
public func beginAuthentication(
107-
challenge: String? = nil,
107+
challenge: EncodedBase64? = nil,
108108
timeout: TimeInterval?,
109109
allowCredentials: [PublicKeyCredentialDescriptor]? = nil,
110110
userVerification: UserVerificationRequirement = .preferred,
@@ -136,7 +136,7 @@ public struct WebAuthnManager {
136136

137137
let response = credential.response
138138

139-
guard let clientDataData = response.clientDataJSON.base64URLDecodedData else {
139+
guard let clientDataData = response.clientDataJSON.urlDecoded.decoded else {
140140
throw WebAuthnError.invalidClientDataJSON
141141
}
142142
let clientData = try JSONDecoder().decode(CollectedClientData.self, from: clientDataData)
@@ -147,7 +147,7 @@ public struct WebAuthnManager {
147147
)
148148
// TODO: - Verify token binding
149149

150-
guard let authenticatorDataBytes = response.authenticatorData.base64URLDecodedData else {
150+
guard let authenticatorDataBytes = response.authenticatorData.urlDecoded.decoded else {
151151
throw WebAuthnError.invalidAuthenticatorData
152152
}
153153
let authenticatorData = try AuthenticatorData(bytes: authenticatorDataBytes)
@@ -175,7 +175,7 @@ public struct WebAuthnManager {
175175
let signatureBase = authenticatorDataBytes + clientDataHash
176176

177177
let credentialPublicKey = try CredentialPublicKey(publicKeyBytes: credentialPublicKey)
178-
guard let signatureData = response.signature.base64URLDecodedData else { throw WebAuthnError.invalidSignature }
178+
guard let signatureData = response.signature.urlDecoded.decoded else { throw WebAuthnError.invalidSignature }
179179
try credentialPublicKey.verify(signature: signatureData, data: signatureBase)
180180

181181
return VerifiedAuthentication(

Tests/WebAuthnTests/HelpersTests.swift

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,14 @@ final class HelpersTests: XCTestCase {
1111
let base64Encoded = input.base64EncodedString()
1212
let base64URLEncoded = input.base64URLEncodedString()
1313

14-
XCTAssertEqual(expectedBase64, base64Encoded)
15-
XCTAssertEqual(expectedBase64URL, base64URLEncoded)
14+
XCTAssertEqual(expectedBase64, base64Encoded.asString())
15+
XCTAssertEqual(expectedBase64URL, base64URLEncoded.asString())
16+
}
17+
18+
func testEncodeBase64Codable() throws {
19+
let base64 = EncodedBase64("AQABAAEBAAEAAQEAAAABAA==")
20+
let json = try JSONEncoder().encode(base64)
21+
let decodedBase64 = try JSONDecoder().decode(EncodedBase64.self, from: json)
22+
XCTAssertEqual(base64, decodedBase64)
1623
}
1724
}

Tests/WebAuthnTests/WebAuthnManagerTests.swift

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ final class WebAuthnManagerTests: XCTestCase {
4949
XCTAssertEqual(options.relyingParty.id, relyingPartyID)
5050
XCTAssertEqual(options.relyingParty.name, relyingPartyDisplayName)
5151
XCTAssertEqual(options.timeout, timeout)
52-
XCTAssertEqual(options.user.id, user.userID.toBase64())
52+
XCTAssertEqual(options.user.id, user.userID.toBase64().asString())
5353
XCTAssertEqual(options.user.displayName, user.displayName)
5454
XCTAssertEqual(options.user.name, user.name)
5555
XCTAssertEqual(options.publicKeyCredentialParameters, [publicKeyCredentialParameter])
@@ -211,31 +211,31 @@ final class WebAuthnManagerTests: XCTestCase {
211211
}
212212

213213
func testFinishRegistrationFailsIfCeremonyTypeDoesNotMatch() async throws {
214-
let clientDataJSONWrongCeremonyType = String.base64URL(fromBase64: """
214+
let clientDataJSONWrongCeremonyType = """
215215
{
216216
"type": "webauthn.get",
217217
"challenge": "cmFuZG9tU3RyaW5nRnJvbVNlcnZlcg",
218218
"origin": "http://localhost:8080",
219219
"crossOrigin": false,
220220
"other_keys_can_be_added_here": "do not compare clientDataJSON against a template. See https://goo.gl/yabPex"
221221
}
222-
""".toBase64())
222+
""".toBase64().urlEncoded
223223
try await assertThrowsError(
224224
await finishRegistration(clientDataJSON: clientDataJSONWrongCeremonyType),
225225
expect: CollectedClientData.CollectedClientDataVerifyError.ceremonyTypeDoesNotMatch
226226
)
227227
}
228228

229229
func testFinishRegistrationFailsIfChallengeDoesNotMatch() async throws {
230-
let clientDataJSONWrongChallenge = String.base64URL(fromBase64: """
230+
let clientDataJSONWrongChallenge = """
231231
{
232232
"type": "webauthn.create",
233233
"challenge": "cmFuZG9tU3RyaW5nRnJvbVNlcnZlcg",
234234
"origin": "http://localhost:8080",
235235
"crossOrigin": false,
236236
"other_keys_can_be_added_here": "do not compare clientDataJSON against a template. See https://goo.gl/yabPex"
237237
}
238-
""".toBase64())
238+
""".toBase64().urlEncoded
239239
try await assertThrowsError(
240240
await finishRegistration(
241241
challenge: "definitelyAnotherChallenge",
@@ -246,15 +246,15 @@ final class WebAuthnManagerTests: XCTestCase {
246246
}
247247

248248
func testFinishRegistrationFailsIfOriginDoesNotMatch() async throws {
249-
let clientDataJSONWrongOrigin: URLEncodedBase64 = String.base64URL(fromBase64: """
249+
let clientDataJSONWrongOrigin: URLEncodedBase64 = """
250250
{
251251
"type": "webauthn.create",
252252
"challenge": "cmFuZG9tU3RyaW5nRnJvbVNlcnZlcg",
253253
"origin": "http://johndoe.com",
254254
"crossOrigin": false,
255255
"other_keys_can_be_added_here": "do not compare clientDataJSON against a template. See https://goo.gl/yabPex"
256256
}
257-
""".toBase64())
257+
""".toBase64().urlEncoded
258258
// `webAuthnManager` is configured with origin = https://example.com
259259
try await assertThrowsError(
260260
await finishRegistration(
@@ -340,18 +340,18 @@ final class WebAuthnManagerTests: XCTestCase {
340340

341341
func testFinishRegistrationFailsIfRawIDIsTooLong() async throws {
342342
try await assertThrowsError(
343-
await finishRegistration(rawID: [UInt8](repeating: 0, count: 1024).base64EncodedString()),
343+
await finishRegistration(rawID: [UInt8](repeating: 0, count: 1024).base64EncodedString().urlEncoded),
344344
expect: WebAuthnError.credentialRawIDTooLong
345345
)
346346
}
347347

348348
private func finishRegistration(
349349
challenge: EncodedBase64 = "cmFuZG9tU3RyaW5nRnJvbVNlcnZlcg",
350-
id: EncodedBase64 = "4PrJNQUJ9xdI2DeCzK9rTBRixhXHDiVdoTROQIh8j80",
350+
id: String = "4PrJNQUJ9xdI2DeCzK9rTBRixhXHDiVdoTROQIh8j80",
351351
type: String = "public-key",
352-
rawID: EncodedBase64 = "4PrJNQUJ9xdI2DeCzK9rTBRixhXHDiVdoTROQIh8j80",
353-
clientDataJSON: String = "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiY21GdVpHOXRVM1J5YVc1blJuSnZiVk5sY25abGNnIiwib3JpZ2luIjoiaHR0cHM6Ly9leGFtcGxlLmNvbSIsImNyb3NzT3JpZ2luIjpmYWxzZSwib3RoZXJfa2V5c19jYW5fYmVfYWRkZWRfaGVyZSI6ImRvIG5vdCBjb21wYXJlIGNsaWVudERhdGFKU09OIGFnYWluc3QgYSB0ZW1wbGF0ZS4gU2VlIGh0dHBzOi8vZ29vLmdsL3lhYlBleCJ9",
354-
attestationObject: String = "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVg5o3mm9u6vuaVeN4wRgDTidR5oL6ufLTCrE9ISVYbOGUdBAAAAAKN5pvbur7mlXjeMEYA04nUAAQAA",
352+
rawID: URLEncodedBase64 = "4PrJNQUJ9xdI2DeCzK9rTBRixhXHDiVdoTROQIh8j80",
353+
clientDataJSON: URLEncodedBase64 = "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiY21GdVpHOXRVM1J5YVc1blJuSnZiVk5sY25abGNnIiwib3JpZ2luIjoiaHR0cHM6Ly9leGFtcGxlLmNvbSIsImNyb3NzT3JpZ2luIjpmYWxzZSwib3RoZXJfa2V5c19jYW5fYmVfYWRkZWRfaGVyZSI6ImRvIG5vdCBjb21wYXJlIGNsaWVudERhdGFKU09OIGFnYWluc3QgYSB0ZW1wbGF0ZS4gU2VlIGh0dHBzOi8vZ29vLmdsL3lhYlBleCJ9",
354+
attestationObject: URLEncodedBase64 = "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVg5o3mm9u6vuaVeN4wRgDTidR5oL6ufLTCrE9ISVYbOGUdBAAAAAKN5pvbur7mlXjeMEYA04nUAAQAA",
355355
requireUserVerification: Bool = false,
356356
confirmCredentialIDNotRegisteredYet: (String) async throws -> Bool = { _ in true }
357357
) async throws -> Credential {

0 commit comments

Comments
 (0)