Skip to content

Commit d734ed9

Browse files
committed
Cleanup base64 conversions
Add EncodedBase64.urlEncoded Add EncodedBase64.decoded Add URLEncodedBase64.urlDecoded
1 parent 4264039 commit d734ed9

File tree

6 files changed

+74
-60
lines changed

6 files changed

+74
-60
lines changed

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.string.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: 56 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@
1515
import Foundation
1616
import Logging
1717

18-
/// Container for URL encoded base64 data
19-
public struct URLEncodedBase64: ExpressibleByStringLiteral, Codable, Hashable {
20-
let string: String
18+
/// Container for base64 encoded data
19+
public struct EncodedBase64: ExpressibleByStringLiteral, Codable, Hashable {
20+
private let base64: String
2121

2222
public init(_ string: String) {
23-
self.string = string
23+
self.base64 = string
2424
}
2525

2626
public init(stringLiteral value: StringLiteralType) {
@@ -29,21 +29,45 @@ public struct URLEncodedBase64: ExpressibleByStringLiteral, Codable, Hashable {
2929

3030
public init(from decoder: Decoder) throws {
3131
let container = try decoder.singleValueContainer()
32-
self.string = try container.decode(String.self)
32+
self.base64 = try container.decode(String.self)
3333
}
3434

3535
public func encode(to encoder: Encoder) throws {
3636
var container = encoder.singleValueContainer()
37-
try container.encode(self.string)
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)
3862
}
3963
}
4064

41-
/// Container for base64 encoded data
42-
public struct EncodedBase64: ExpressibleByStringLiteral, Codable, Hashable {
43-
let string: String
65+
/// Container for URL encoded base64 data
66+
public struct URLEncodedBase64: ExpressibleByStringLiteral, Codable, Hashable {
67+
let base64: String
4468

4569
public init(_ string: String) {
46-
self.string = string
70+
self.base64 = string
4771
}
4872

4973
public init(stringLiteral value: StringLiteralType) {
@@ -52,24 +76,40 @@ public struct EncodedBase64: ExpressibleByStringLiteral, Codable, Hashable {
5276

5377
public init(from decoder: Decoder) throws {
5478
let container = try decoder.singleValueContainer()
55-
self.string = try container.decode(String.self)
79+
self.base64 = try container.decode(String.self)
5680
}
5781

5882
public func encode(to encoder: Encoder) throws {
5983
var container = encoder.singleValueContainer()
60-
try container.encode(self.string)
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)
6194
}
62-
}
6395

64-
//public typealias URLEncodedBase64 = String
65-
//public typealias EncodedBase64 = String
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+
}
66106

67107
extension Array where Element == UInt8 {
68108
/// Encodes an array of bytes into a base64url-encoded string
69109
/// - Returns: A base64url-encoded string
70110
public func base64URLEncodedString() -> URLEncodedBase64 {
71111
let base64String = Data(bytes: self, count: self.count).base64EncodedString()
72-
return String.base64URL(fromBase64: .init(base64String))
112+
return EncodedBase64(base64String).urlEncoded
73113
}
74114

75115
/// Encodes an array of bytes into a base64 string
@@ -88,33 +128,7 @@ extension Data {
88128
}
89129

90130
extension String {
91-
/// Decode a base64url-encoded `String` to a base64 `String`
92-
/// - Returns: A base64-encoded `String`
93-
public static func base64(fromBase64URLEncoded base64URLEncoded: URLEncodedBase64) -> EncodedBase64 {
94-
return .init(
95-
base64URLEncoded.string.replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/")
96-
)
97-
}
98-
99-
public static func base64URL(fromBase64 base64Encoded: EncodedBase64) -> URLEncodedBase64 {
100-
return .init(
101-
base64Encoded.string.replacingOccurrences(of: "+", with: "-")
102-
.replacingOccurrences(of: "/", with: "_")
103-
.replacingOccurrences(of: "=", with: "")
104-
)
105-
}
106-
107131
func toBase64() -> EncodedBase64 {
108132
return .init(Data(self.utf8).base64EncodedString())
109133
}
110134
}
111-
112-
extension URLEncodedBase64 {
113-
public var base64URLDecodedData: Data? {
114-
var result = self.string.replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/")
115-
while result.count % 4 != 0 {
116-
result = result.appending("=")
117-
}
118-
return Data(base64Encoded: result)
119-
}
120-
}

Sources/WebAuthn/WebAuthnManager.swift

Lines changed: 4 additions & 4 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
@@ -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: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ final class HelpersTests: XCTestCase {
1111
let base64Encoded = input.base64EncodedString()
1212
let base64URLEncoded = input.base64URLEncodedString()
1313

14-
XCTAssertEqual(expectedBase64, base64Encoded.string)
15-
XCTAssertEqual(expectedBase64URL, base64URLEncoded.string)
14+
XCTAssertEqual(expectedBase64, base64Encoded.asString())
15+
XCTAssertEqual(expectedBase64URL, base64URLEncoded.asString())
1616
}
1717

1818
func testEncodeBase64Codable() throws {

Tests/WebAuthnTests/WebAuthnManagerTests.swift

Lines changed: 8 additions & 8 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().string)
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,7 +340,7 @@ final class WebAuthnManagerTests: XCTestCase {
340340

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

0 commit comments

Comments
 (0)