Skip to content
Open
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,6 @@ timeline.xctimeline
playground.xcworkspace

.DS_Store

## Swift Package Manager
.swiftpm/
8 changes: 6 additions & 2 deletions Sources/nfclib/CardActions/CardReaderNFC.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,12 @@ class CardReaderNFC: CardReader {
var ksMac: Bytes = AES.Zero
var SSC: Bytes = AES.Zero

init(_ tag: NFCISO7816Tag, CAN: String) async throws {
self.tag = SendableISO7816Tag(tag: tag)
init(
_ tag: NFCISO7816Tag,
CAN: String,
invalidationError: @escaping @Sendable @MainActor () -> Error? = { nil }
) async throws {
self.tag = SendableISO7816Tag(tag: tag, invalidationError: invalidationError)

CardReaderNFC.logger.debug("Select CardAccess")
_ = try await self.tag.sendCommand(cls: 0x00, ins: 0xA4, p1Byte: 0x02, p2Byte: 0x0C, data: Data([0x01, 0x1C]))
Expand Down
19 changes: 13 additions & 6 deletions Sources/nfclib/Extensions/NFCISO7816Tag+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import CryptoTokenKit
@MainActor
struct SendableISO7816Tag {
var tag: NFCISO7816Tag
/// Why the reader session ended, if it has. Reported instead of the transceive error it causes.
var invalidationError: @Sendable @MainActor () -> Error? = { nil }
}

extension SendableISO7816Tag {
Expand All @@ -28,14 +30,19 @@ extension SendableISO7816Tag {
data: data,
expectedResponseLength: leByte
)
let result: (Data, UInt8, UInt8) = try await withCheckedThrowingContinuation { continuation in
tag.sendCommand(apdu: apdu) { responseData, sw1, sw2, error in
if let error {
continuation.resume(throwing: error)
} else {
continuation.resume(returning: (responseData, sw1, sw2))
let result: (Data, UInt8, UInt8)
do {
result = try await withCheckedThrowingContinuation { continuation in
tag.sendCommand(apdu: apdu) { responseData, sw1, sw2, error in
if let error {
continuation.resume(throwing: error)
} else {
continuation.resume(returning: (responseData, sw1, sw2))
}
}
}
} catch {
throw invalidationError() ?? error
}
switch result {
case (_, 0x63, 0x00):
Expand Down
136 changes: 104 additions & 32 deletions Sources/nfclib/Operations/NFCConnection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,51 +4,89 @@
import Foundation
@preconcurrency import CoreNFC

// Allow CoreNFC types to cross async boundaries in this controlled context
extension NFCTagReaderSession: @unchecked @retroactive Sendable {}
extension NFCTag: @unchecked @retroactive Sendable {}

@MainActor
public class NFCConnection {
public init() {}
public func setup(_ session: NFCTagReaderSession, tags: [NFCTag]) async throws -> NFCISO7816Tag {
if tags.count > 1 {
session.invalidate(errorMessage: "Failed to read data")
throw IdCardInternalError.multipleTagsDetected
}
public class NFCConnection: NSObject, @MainActor NFCTagReaderSessionDelegate {
private var session: NFCTagReaderSession?
private var continuation: CheckedContinuation<NFCISO7816Tag, Error>?
private var invalidationError: Error?

guard let firstTag = tags.first else {
session.invalidate(errorMessage: "Failed to read data")
throw IdCardInternalError.invalidTag
}
override private init() {}

/// Runs an operation on a new connection. On success the session is closed with `successMessage`,
/// on failure `onError` maps the error to the error that is thrown and the message the session is
/// invalidated with. Cancelling the calling task invalidates the session, and any failure after
/// cancellation is passed to `onError` as `cancelledByUser`. A failure after the session was
/// invalidated (e.g. the user cancelled the sheet or it timed out) is passed as that reason.
public static func run<T>(
successMessage: String = "Data read",
onError: (Error) -> (error: Error, message: String) = { (error: $0, message: "Failed to read data") },
_ operation: (NFCConnection) async throws -> T
) async throws -> T {
let connection = NFCConnection()
defer { connection.session = nil }
do {
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
session.connect(to: firstTag) { error in
if let error {
continuation.resume(throwing: error)
} else {
continuation.resume()
}
}
let result = try await withTaskCancellationHandler {
try await operation(connection)
} onCancel: {
Task { @MainActor [weak connection] in connection?.session?.invalidate() }
}
connection.session?.alertMessage = successMessage
connection.session?.invalidate()
return result
} catch {
session.invalidate(errorMessage: "Failed to read data")
throw IdCardInternalError.connectionFailed
let error: Error = if Task.isCancelled || error is CancellationError {
IdCardInternalError.cancelledByUser
} else {
connection.invalidationError ?? error
}
let failure = onError(error)
connection.session?.invalidate(errorMessage: failure.message)
throw failure.error
}
}

guard case let .iso7816(tag) = firstTag else {
session.invalidate(errorMessage: "Failed to read data")
throw IdCardInternalError.invalidTag
/// Starts the session showing `message`, and once a card is detected shows `detectedMessage`
/// while it is connected to and set up with `CAN`.
public func connect(CAN: String, message: String, detectedMessage: String) async throws -> CardCommands {
try Task.checkCancellation()
guard NFCTagReaderSession.readingAvailable else {
throw IdCardInternalError.nfcNotSupported
}
guard self.session == nil else { throw IdCardInternalError.operationInProgress }
guard let session = NFCTagReaderSession(pollingOption: .iso14443, delegate: self, queue: DispatchQueue.main) else {
throw IdCardInternalError.nfcNotSupported
}
session.alertMessage = message
self.session = session
invalidationError = nil
return try await withTaskCancellationHandler {
let tag = try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<NFCISO7816Tag, Error>) in
self.continuation = continuation
session.begin()
}
session.alertMessage = detectedMessage
do {
try await session.connect(to: .iso7816(tag))
} catch {
throw invalidationError ?? IdCardInternalError.connectionFailed
}
return try await cardCommands(tag: tag, CAN: CAN)
} onCancel: {
Task { @MainActor [weak self] in self?.session?.invalidate() }
}

return tag
}

@MainActor
public func getCardCommands(_: NFCTagReaderSession, tag: NFCISO7816Tag, CAN: String) async throws -> CardCommands {
let initialSelectedAID = tag.initialSelectedAID
let reader = try await CardReaderNFC(tag, CAN: CAN)
guard let aid = Bytes(hex: initialSelectedAID) else {
private func cardCommands(tag: NFCISO7816Tag, CAN: String) async throws -> CardCommands {
guard let aid = Bytes(hex: tag.initialSelectedAID), !aid.isEmpty else {
throw IdCardInternalError.connectionFailed
}

let reader = try await CardReaderNFC(tag, CAN: CAN) { [weak self] in self?.invalidationError }

// Try Idemia with explicit AID
if let cmd = Idemia(reader: reader, aid: aid) {
return cmd
Expand All @@ -68,5 +106,39 @@ public class NFCConnection {

throw IdCardInternalError.cardNotSupported
}
}

public func message(_ message: String) {
session?.alertMessage = message
}

public func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) {
guard tags.count == 1, let firstTag = tags.first else {
session.alertMessage = "More than one card detected, please present only your ID card"
Task { @MainActor [weak self] in
// Give the user time to remove the extra card before polling again
try? await Task.sleep(for: .milliseconds(500))
self?.session?.restartPolling()
}
return
}
guard case let .iso7816(tag) = firstTag else {
session.alertMessage = "Card not supported, please present your ID card"
session.restartPolling()
return
}
continuation?.resume(returning: tag)
continuation = nil
}

public func tagReaderSessionDidBecomeActive(_: NFCTagReaderSession) { }

public func tagReaderSession(_ session: NFCTagReaderSession, didInvalidateWithError error: Error) {
guard session === self.session else { return }
self.session = nil
let error = IdCardInternalError.mapSessionInvalidation(error)
invalidationError = error
guard let continuation else { return }
self.continuation = nil
continuation.resume(throwing: error)
}
}
91 changes: 13 additions & 78 deletions Sources/nfclib/Operations/OperationReadCertificate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@
// SPDX-License-Identifier: LGPL-2.1-or-later

import Foundation
import CoreNFC
import Security


// MARK: - Local Types

public enum ReadCertificateError: Error {
Expand All @@ -19,89 +17,26 @@ public enum CertificateUsage {
case sign
}

// Allow CoreNFC types to cross async boundaries in this controlled context
extension NFCTagReaderSession: @unchecked @retroactive Sendable {}
extension NFCTag: @unchecked @retroactive Sendable {}

@MainActor
public class OperationReadCertificate: NSObject {
private var session: NFCTagReaderSession?
private var CAN: String = ""
private var certUsage: CertificateUsage!
private let nfcMessage: String = "Please place your ID card against the smart device"
private let connection = NFCConnection()
private var continuation: CheckedContinuation<SecCertificate, Error>?
public class OperationReadCertificate {
public init() {}

public func startReading(CAN: String, certUsage: CertificateUsage) async throws -> SecCertificate {

return try await withCheckedThrowingContinuation { continuation in
guard self.continuation == nil else {
continuation.resume(throwing: IdCardInternalError.operationInProgress)
return
}
self.continuation = continuation

guard NFCTagReaderSession.readingAvailable else {
self.finish(.failure(IdCardInternalError.nfcNotSupported))
return
}

self.CAN = CAN
self.certUsage = certUsage
session = NFCTagReaderSession(pollingOption: .iso14443, delegate: self, queue: DispatchQueue.main)
session?.alertMessage = nfcMessage
session?.begin()
}
}

private func finish(_ result: Result<SecCertificate, Error>) {
guard let continuation else { return }
self.continuation = nil
continuation.resume(with: result)
}
}

extension OperationReadCertificate: @MainActor NFCTagReaderSessionDelegate {
public func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) {
Task { @MainActor in
guard let certUsage else {
finish(.failure(ReadCertificateError.certificateUsageNotSpecified))
session.invalidate(errorMessage: "Failed to read data")
return
}
try await NFCConnection.run { connection in
let cardCommands = try await connection.connect(
CAN: CAN,
message: "Please place your ID card against the smart device",
detectedMessage: "Hold your ID card against your smart device until the data is read"
)
do {
session.alertMessage = "Hold your ID card against your smart device until the data is read"
let tag = try await connection.setup(session, tags: tags)
let cardCommands = try await connection.getCardCommands(session, tag: tag, CAN: CAN)
do {
switch certUsage {
case .auth:
let cert = try await cardCommands.readAuthenticationCertificate()
let x509Certificate = try convertBytesToX509Certificate(cert)
finish(.success(x509Certificate))
case .sign:
let cert = try await cardCommands.readSignatureCertificate()
let x509Certificate = try convertBytesToX509Certificate(cert)
finish(.success(x509Certificate))
}
session.alertMessage = "Data read"
session.invalidate()
} catch {
finish(.failure(ReadCertificateError.failedToReadCertificate))
session.invalidate(errorMessage: "Failed to read data")
let cert = switch certUsage {
case .auth: try await cardCommands.readAuthenticationCertificate()
case .sign: try await cardCommands.readSignatureCertificate()
}
return try convertBytesToX509Certificate(cert)
} catch {
finish(.failure(error))
session.invalidate(errorMessage: "Failed to read data")
throw ReadCertificateError.failedToReadCertificate
}
}
}

public func tagReaderSessionDidBecomeActive(_: NFCTagReaderSession) { }

public func tagReaderSession(_ session: NFCTagReaderSession, didInvalidateWithError error: Error) {
guard session === self.session else { return }
self.session = nil
finish(.failure(IdCardInternalError.mapSessionInvalidation(error)))
}
}
Loading
Loading