Skip to content

Swift 6 #106

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Feb 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version:5.9
// swift-tools-version: 6.0
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift WebAuthn open source project
Expand Down Expand Up @@ -36,15 +36,13 @@ let package = Package(
.product(name: "Crypto", package: "swift-crypto"),
.product(name: "_CryptoExtras", package: "swift-crypto"),
.product(name: "Logging", package: "swift-log"),
],
swiftSettings: [.enableExperimentalFeature("StrictConcurrency=complete")]
]
),
.testTarget(
name: "WebAuthnTests",
dependencies: [
.target(name: "WebAuthn")
],
swiftSettings: [.enableExperimentalFeature("StrictConcurrency=complete")]
]
)
]
)
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public struct AuthenticationCredential: Sendable {
}

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

id = try container.decode(URLEncodedBase64.self, forKey: .id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public struct AuthenticatorAssertionResponse: Sendable {
}

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

clientDataJSON = try container.decodeBytesFromURLEncodedBase64(forKey: .clientDataJSON)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public struct PublicKeyCredentialRequestOptions: Encodable, Sendable {

// let extensions: [String: Any]

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

try container.encode(challenge.base64URLEncodedString(), forKey: .challenge)
Expand Down Expand Up @@ -107,7 +107,7 @@ public struct PublicKeyCredentialDescriptor: Equatable, Encodable, Sendable {
self.transports = transports
}

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

try container.encode(type, forKey: .type)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public struct AuthenticatorAttestationResponse: Sendable {
}

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

clientDataJSON = try container.decodeBytesFromURLEncodedBase64(forKey: .clientDataJSON)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public struct PublicKeyCredentialCreationOptions: Encodable, Sendable {
/// supported.
public let attestation: AttestationConveyancePreference

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

try container.encode(challenge.base64URLEncodedString(), forKey: .challenge)
Expand Down Expand Up @@ -142,7 +142,7 @@ public struct PublicKeyCredentialUserEntity: Encodable, Sendable {
self.displayName = displayName
}

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

try container.encode(id.base64URLEncodedString(), forKey: .id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public struct RegistrationCredential: Sendable {
}

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

id = try container.decode(URLEncodedBase64.self, forKey: .id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,12 @@ public struct AuthenticatorAttestationGloballyUniqueID: Hashable, Sendable {
public typealias AAGUID = AuthenticatorAttestationGloballyUniqueID

extension AuthenticatorAttestationGloballyUniqueID: Codable {
public init(from decoder: Decoder) throws {
public init(from decoder: any Decoder) throws {
let container = try decoder.singleValueContainer()
id = try container.decode(UUID.self)
}

public func encode(to encoder: Encoder) throws {
public func encode(to encoder: any Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(id)
}
Expand Down
36 changes: 19 additions & 17 deletions Sources/WebAuthn/Ceremonies/Shared/AuthenticatorData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ struct AuthenticatorData: Equatable, Sendable {
}

extension AuthenticatorData {
init(bytes: [UInt8]) throws {
init(bytes: [UInt8]) throws(WebAuthnError) {
let minAuthDataLength = 37
guard bytes.count >= minAuthDataLength else {
throw WebAuthnError.authDataTooShort
throw .authDataTooShort
}

let relyingPartyIDHash = Array(bytes[..<32])
Expand All @@ -44,29 +44,29 @@ extension AuthenticatorData {
if flags.attestedCredentialData {
let minAttestedAuthLength = 37 + AAGUID.size + 2
guard bytes.count > minAttestedAuthLength else {
throw WebAuthnError.attestedCredentialDataMissing
throw .attestedCredentialDataMissing
}
let (data, length) = try Self.parseAttestedData(bytes)
attestedCredentialData = data
remainingCount -= length
// For assertion signatures, the AT flag MUST NOT be set and the attestedCredentialData MUST NOT be included.
} else {
if !flags.extensionDataIncluded && bytes.count != minAuthDataLength {
throw WebAuthnError.attestedCredentialFlagNotSet
throw .attestedCredentialFlagNotSet
}
}

var extensionData: [UInt8]?
if flags.extensionDataIncluded {
guard remainingCount != 0 else {
throw WebAuthnError.extensionDataMissing
throw .extensionDataMissing
}
extensionData = Array(bytes[(bytes.count - remainingCount)...])
remainingCount -= extensionData!.count
}

guard remainingCount == 0 else {
throw WebAuthnError.leftOverBytesInAuthenticatorData
throw .leftOverBytesInAuthenticatorData
}

self.relyingPartyIDHash = relyingPartyIDHash
Expand All @@ -81,31 +81,33 @@ extension AuthenticatorData {
///
/// This is assumed to take place after the first 37 bytes of `data`, which is always of fixed size.
/// - SeeAlso: [WebAuthn Level 3 Editor's Draft §6.5.1. Attested Credential Data]( https://w3c.github.io/webauthn/#sctn-attested-credential-data)
private static func parseAttestedData(_ data: [UInt8]) throws -> (AttestedCredentialData, Int) {
private static func parseAttestedData(_ data: [UInt8]) throws(WebAuthnError) -> (AttestedCredentialData, Int) {
/// **aaguid** (16): The AAGUID of the authenticator.
guard let aaguid = AAGUID(bytes: data[37..<(37 + AAGUID.size)]) // Bytes [37-52]
else { throw WebAuthnError.attestedCredentialDataMissing }
else { throw .attestedCredentialDataMissing }

/// **credentialIdLength** (2): Byte length L of credentialId, 16-bit unsigned big-endian integer. Value MUST be ≤ 1023.
let idLengthBytes = data[53..<55] // Length is 2 bytes
let idLengthData = Data(idLengthBytes)
let idLength = UInt16(bigEndianBytes: idLengthData)

guard idLength <= 1023
else { throw WebAuthnError.credentialIDTooLong }
else { throw .credentialIDTooLong }

let credentialIDEndIndex = Int(idLength) + 55
guard data.count >= credentialIDEndIndex
else { throw WebAuthnError.credentialIDTooShort }
else { throw .credentialIDTooShort }

/// **credentialId** (L): Credential ID
let credentialID = data[55..<credentialIDEndIndex]

/// **credentialPublicKey** (variable): The credential public key encoded in `COSE_Key` format, as defined in [Section 7](https://tools.ietf.org/html/rfc9052#section-7) of [RFC9052], using the CTAP2 canonical CBOR encoding form.
/// Assuming valid CBOR, verify the public key's length by decoding the next CBOR item.
/// Assuming valid CBOR, verify the public key's length by decoding the next CBOR item, and checking how much data is left on the stream.
let inputStream = ByteInputStream(data[credentialIDEndIndex...])
let decoder = CBORDecoder(stream: inputStream, options: CBOROptions(maximumDepth: 16))
_ = try decoder.decodeItem()
do {
let decoder = CBORDecoder(stream: inputStream, options: CBOROptions(maximumDepth: 16))
_ = try decoder.decodeItem()
} catch { throw .invalidPublicKeyLength }
Comment on lines -105 to +110
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Took a chance to define a new error case for this and clarify what is happening.

let publicKeyBytes = data[credentialIDEndIndex..<(data.count - inputStream.remainingBytes)]

let data = AttestedCredentialData(
Expand All @@ -132,13 +134,13 @@ class ByteInputStream: CBORInputStream {
/// The remaining bytes in the original data buffer.
var remainingBytes: Int { slice.count }

func popByte() throws -> UInt8 {
if slice.count < 1 { throw CBORError.unfinishedSequence }
func popByte() throws(CBORError) -> UInt8 {
if slice.count < 1 { throw .unfinishedSequence }
return slice.removeFirst()
}

func popBytes(_ n: Int) throws -> ArraySlice<UInt8> {
if slice.count < n { throw CBORError.unfinishedSequence }
func popBytes(_ n: Int) throws(CBORError) -> ArraySlice<UInt8> {
if slice.count < n { throw .unfinishedSequence }
let result = slice.prefix(n)
slice = slice.dropFirst(n)
return result
Expand Down
8 changes: 4 additions & 4 deletions Sources/WebAuthn/Ceremonies/Shared/CollectedClientData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@ public struct CollectedClientData: Codable, Hashable, Sendable {
public let challenge: URLEncodedBase64
public let origin: String

func verify(storedChallenge: [UInt8], ceremonyType: CeremonyType, relyingPartyOrigin: String) throws {
guard type == ceremonyType else { throw CollectedClientDataVerifyError.ceremonyTypeDoesNotMatch }
func verify(storedChallenge: [UInt8], ceremonyType: CeremonyType, relyingPartyOrigin: String) throws(CollectedClientDataVerifyError) {
guard type == ceremonyType else { throw .ceremonyTypeDoesNotMatch }
guard challenge == storedChallenge.base64URLEncodedString() else {
throw CollectedClientDataVerifyError.challengeDoesNotMatch
throw .challengeDoesNotMatch
}
guard origin == relyingPartyOrigin else { throw CollectedClientDataVerifyError.originDoesNotMatch }
guard origin == relyingPartyOrigin else { throw .originDoesNotMatch }
}
}
20 changes: 10 additions & 10 deletions Sources/WebAuthn/Ceremonies/Shared/CredentialPublicKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ struct EC2PublicKey: PublicKey, Sendable {
self.yCoordinate = yCoordinate
}

init(publicKeyObject: CBOR, algorithm: COSEAlgorithmIdentifier) throws {
init(publicKeyObject: CBOR, algorithm: COSEAlgorithmIdentifier) throws(WebAuthnError) {
self.algorithm = algorithm

// Curve is key -1 - or -0 for SwiftCBOR
Expand All @@ -117,18 +117,18 @@ struct EC2PublicKey: PublicKey, Sendable {
guard let curveRaw = publicKeyObject[COSEKey.crv.cbor],
case let .unsignedInt(curve) = curveRaw,
let coseCurve = COSECurve(rawValue: curve) else {
throw WebAuthnError.invalidCurve
throw .invalidCurve
}
self.curve = coseCurve

guard let xCoordRaw = publicKeyObject[COSEKey.x.cbor],
case let .byteString(xCoordinateBytes) = xCoordRaw else {
throw WebAuthnError.invalidXCoordinate
throw .invalidXCoordinate
}
xCoordinate = xCoordinateBytes
guard let yCoordRaw = publicKeyObject[COSEKey.y.cbor],
case let .byteString(yCoordinateBytes) = yCoordRaw else {
throw WebAuthnError.invalidYCoordinate
throw .invalidYCoordinate
}
yCoordinate = yCoordinateBytes
}
Expand Down Expand Up @@ -167,18 +167,18 @@ struct RSAPublicKeyData: PublicKey, Sendable {

var rawRepresentation: [UInt8] { n + e }

init(publicKeyObject: CBOR, algorithm: COSEAlgorithmIdentifier) throws {
init(publicKeyObject: CBOR, algorithm: COSEAlgorithmIdentifier) throws(WebAuthnError) {
self.algorithm = algorithm

guard let nRaw = publicKeyObject[COSEKey.n.cbor],
case let .byteString(nBytes) = nRaw else {
throw WebAuthnError.invalidModulus
throw .invalidModulus
}
n = nBytes

guard let eRaw = publicKeyObject[COSEKey.e.cbor],
case let .byteString(eBytes) = eRaw else {
throw WebAuthnError.invalidExponent
throw .invalidExponent
}
e = eBytes
}
Expand Down Expand Up @@ -213,17 +213,17 @@ struct OKPPublicKey: PublicKey, Sendable {
let curve: UInt64
let xCoordinate: [UInt8]

init(publicKeyObject: CBOR, algorithm: COSEAlgorithmIdentifier) throws {
init(publicKeyObject: CBOR, algorithm: COSEAlgorithmIdentifier) throws(WebAuthnError) {
self.algorithm = algorithm
// Curve is key -1, or NegativeInt 0 for SwiftCBOR
guard let curveRaw = publicKeyObject[.negativeInt(0)], case let .unsignedInt(curve) = curveRaw else {
throw WebAuthnError.invalidCurve
throw .invalidCurve
}
self.curve = curve
// X Coordinate is key -2, or NegativeInt 1 for SwiftCBOR
guard let xCoordRaw = publicKeyObject[.negativeInt(1)],
case let .byteString(xCoordinateBytes) = xCoordRaw else {
throw WebAuthnError.invalidXCoordinate
throw .invalidXCoordinate
}
xCoordinate = xCoordinateBytes
}
Expand Down
4 changes: 2 additions & 2 deletions Sources/WebAuthn/Helpers/Base64Utilities.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@ public struct EncodedBase64: ExpressibleByStringLiteral, Codable, Hashable, Equa
self.init(value)
}

public init(from decoder: Decoder) throws {
public init(from decoder: any Decoder) throws {
let container = try decoder.singleValueContainer()
self.base64 = try container.decode(String.self)
}

public func encode(to encoder: Encoder) throws {
public func encode(to encoder: any Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(self.base64)
}
Expand Down
2 changes: 2 additions & 0 deletions Sources/WebAuthn/WebAuthnError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ public struct WebAuthnError: Error, Hashable, Sendable {
case leftOverBytesInAuthenticatorData
case credentialIDTooLong
case credentialIDTooShort
case invalidPublicKeyLength

// MARK: CredentialPublicKey
case badPublicKeyBytes
Expand Down Expand Up @@ -110,6 +111,7 @@ public struct WebAuthnError: Error, Hashable, Sendable {
public static let leftOverBytesInAuthenticatorData = Self(reason: .leftOverBytesInAuthenticatorData)
public static let credentialIDTooLong = Self(reason: .credentialIDTooLong)
public static let credentialIDTooShort = Self(reason: .credentialIDTooShort)
public static let invalidPublicKeyLength = Self(reason: .invalidPublicKeyLength)

// MARK: CredentialPublicKey
public static let badPublicKeyBytes = Self(reason: .badPublicKeyBytes)
Expand Down
2 changes: 1 addition & 1 deletion Sources/WebAuthn/WebAuthnManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ public struct WebAuthnManager: Sendable {
timeout: Duration? = .seconds(60),
allowCredentials: [PublicKeyCredentialDescriptor]? = nil,
userVerification: UserVerificationRequirement = .preferred
) throws -> PublicKeyCredentialRequestOptions {
) -> PublicKeyCredentialRequestOptions {
let challenge = challengeGenerator.generate()

return PublicKeyCredentialRequestOptions(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ final class WebAuthnManagerAuthenticationTests: XCTestCase {

func testBeginAuthentication() async throws {
let allowCredentials: [PublicKeyCredentialDescriptor] = [.init(type: .publicKey, id: [1, 0, 2, 30])]
let options = try webAuthnManager.beginAuthentication(
let options = webAuthnManager.beginAuthentication(
timeout: .seconds(1234),
allowCredentials: allowCredentials,
userVerification: .preferred
Expand Down
2 changes: 1 addition & 1 deletion Tests/WebAuthnTests/WebAuthnManagerIntegrationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ final class WebAuthnManagerIntegrationTests: XCTestCase {
id: [UInt8](URLEncodedBase64(credential.id).urlDecoded.decoded!)
)]

let authenticationOptions = try webAuthnManager.beginAuthentication(
let authenticationOptions = webAuthnManager.beginAuthentication(
timeout: authenticationTimeout,
allowCredentials: rememberedCredentials,
userVerification: userVerification
Expand Down