-
Notifications
You must be signed in to change notification settings - Fork 0
Working with NFC
Aside from signing and decrypting documents with ID-card, you can use IdCardLib to read data from ID-card using NFC antennae in iOS device.
You don't need to worry about handling connections with NFC antennae. IdCardLib will do that for you. Whenever you call a method, that needs interaction with NFC antennae, IdCardLib will attempt to create a tunnel and connect to NFC chip on ID card. When no ID card with NFC chip is present in device vicinity, it will ask user to hold card near the iOS device antennae.
NFCISO7816Tag - https://developer.apple.com/documentation/corenfc/nfciso7816tag
// 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 func startReading(CAN: String, certUsage: CertificateUsage) async throws -> SecCertificate {
return try await withCheckedThrowingContinuation { continuation in
self.continuation = continuation
guard NFCTagReaderSession.readingAvailable else {
continuation.resume(throwing: IdCardInternalError.nfcNotSupported)
return
}
self.CAN = CAN
self.certUsage = certUsage
session = NFCTagReaderSession(pollingOption: .iso14443, delegate: self)
session?.alertMessage = nfcMessage
session?.begin()
}
}
}
extension OperationReadCertificate: NFCTagReaderSessionDelegate {
public func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) {
Task { @MainActor in
defer {
self.session = nil
}
guard let certUsage else {
continuation?.resume(throwing: ReadCertificateError.certificateUsageNotSpecified)
session.invalidate(errorMessage: "Failed to read data")
return
}
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)
continuation?.resume(with: .success(x509Certificate))
case .sign:
let cert = try await cardCommands.readSignatureCertificate()
let x509Certificate = try convertBytesToX509Certificate(cert)
continuation?.resume(with: .success(x509Certificate))
}
session.alertMessage = "Data read"
session.invalidate()
} catch {
session.invalidate(errorMessage: "Failed to read data")
continuation?.resume(throwing: ReadCertificateError.failedToReadCertificate)
}
} catch {
session.invalidate(errorMessage: "Failed to read data")
continuation?.resume(throwing: error)
}
}
}
public func tagReaderSessionDidBecomeActive(_: NFCTagReaderSession) { }
public func tagReaderSession(_: NFCTagReaderSession, didInvalidateWithError _: Error) {
Task { @MainActor in
self.session = nil
}
}
}You can get some data from ID card, that is publicly available. This includes things like card owner name, birth date, document number etc.
let tag = try await connection.setup(session, tags: tags)
let cardCommands = try await connection.getCardCommands(session, tag: tag, CAN: CAN)
let cardInfo = try await cardCommands.readPublicData()Reading data from card can be time-consuming. If you don't need all available data, then you can choose to get only the set of most important values.
func readPublicData() async throws -> CardInfopublic struct CardInfo: Sendable, Hashable {
public var givenName: String
public var surname: String
public var personalCode: String
public var citizenship: String
public var documentNumber: String
public var dateOfExpiry: String
}There are two certificates on ID card - authentication and signing certificate. You can get some basic data like expiry dates from them.
For signing certificate use
func readSignatureCertificate() async throws -> Datalet cert = try await cardCommands.readSignatureCertificate()
let x509Certificate = try convertBytesToX509Certificate(cert)
continuation?.resume(with: .success(x509Certificate))For authentication certificate use
func readAuthenticationCertificate() async throws -> Datalet cert = try await cardCommands.readAuthenticationCertificate()
let x509Certificate = try convertBytesToX509Certificate(cert)
continuation?.resume(with: .success(x509Certificate))PIN and PUK codes can get blocked when invalid code is used for some actions. It is important to notify user in that case, so they would know when they have to take action in order to unblock blocked code. It is not possible to continue to use certain functionality, like document signing, when PIN is blocked.
IdCardLib provides methods for getting retry counter value from ID card. The number returned represents how many retries user has left for each code. When count reaches 0, then that particular code is blocked and can't be used anymore until user unblocks it. Unblock actions are not available for third-party applications. User will have to use one of RIA DigiDoc applications to unblock.
let pin1Response = try await cardCommands.readCodeTryCounterRecord(.pin1)
OperationReadCardData.logger().info("PIN1 retry count: \(pin1Response.retryCount)")
OperationReadCardData.logger().info("PIN1 active: \(pin1Response.pinActive)")
let pin2Response = try await cardCommands.readCodeTryCounterRecord(.pin2)
OperationReadCardData.logger().info("PIN2 retry count: \(pin2Response.retryCount)")
OperationReadCardData.logger().info("PIN2 active: \(pin2Response.pinActive)")
let pukResponse = try await cardCommands.readCodeTryCounterRecord(.puk)
OperationReadCardData.logger().info("PUK retry count: \(pukResponse.retryCount)")
OperationReadCardData.logger().info("PUK active: \(pukResponse.pinActive)")
let pinResponse = PinResponse(
pin1RetryCount: pin1Response.retryCount,
pin1Active: pin1Response.pinActive,
pin2RetryCount: pin2Response.retryCount,
pin2Active: pin2Response.pinActive,
pukRetryCount: pukResponse.retryCount,
pukActive: pukResponse.pinActive,
)Changing PIN1, PIN2, PUK with old code:
let tag = try await self.connection.setup(session, tags: tags)
let cardCommands = try await self.connection.getCardCommands(session, tag: tag, CAN: self.canNumber)
try await cardCommands.changeCode(codeType, to: newPin, verifyCode: currentPin)Unblocking PIN1 and PIN2 with PUK code:
let tag = try await self.connection.setup(session, tags: tags)
let cardCommands = try await self.connection.getCardCommands(session, tag: tag, CAN: self.canNumber)
try await cardCommands.unblockCode(codeType, puk: puk, newCode: newPin)