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
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ public protocol CardCommands: Sendable {
* - Throws: An error if the operation fails.
* - Returns: The remaining attempts as an `UInt8`.
*/
func readCodeTryCounterRecord(_ type: CodeType) async throws -> UInt8
func readCodeTryCounterRecord(_ type: CodeType) async throws -> (retryCount: UInt8, pinActive: Bool)

/**
* Changes the PIN or PUK code.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ extension CardReader {
* - Returns: The full response data returned by the card (excluding the status word).
*/
func sendAPDU(cls: UInt8 = 0x00, ins: UInt8, p1Byte: UInt8 = 0x00, p2Byte: UInt8 = 0x00,
data: (any RangeReplaceableCollection<UInt8>)? = nil, leByte: UInt8? = nil) async throws -> Data {
data: (any Collection<UInt8>)? = nil, leByte: UInt8? = nil) async throws -> Data {
var apdu: Bytes = [cls, ins, p1Byte, p2Byte]
if let data {
apdu.append(UInt8(data.count))
Expand Down
151 changes: 126 additions & 25 deletions Modules/IdCardLib/Sources/IdCardLib/CardActions/CardReaderNFC.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import CryptoTokenKit
internal import SwiftECC
import BigInt

// swiftlint:disable force_unwrapping
class CardReaderNFC: @unchecked CardReader, Loggable {
// swiftlint:disable identifier_name
enum PasswordType: UInt8 {
Expand All @@ -38,6 +39,7 @@ class CardReaderNFC: @unchecked CardReader, Loggable {
}
enum MappingType: String {
case id_PACE_ECDH_GM_AES_CBC_CMAC_256 = "04007f00070202040204" // 0.4.0.127.0.7.2.2.4.2.4
case id_PACE_ECDH_IM_AES_CBC_CMAC_256 = "04007f00070202040404" // 0.4.0.127.0.7.2.2.4.4.4
var data: Data {
guard let value = Data(hex: rawValue) else { return Data() }
return value
Expand Down Expand Up @@ -107,30 +109,34 @@ class CardReaderNFC: @unchecked CardReader, Loggable {
CardReaderNFC.logger().info("Nonce \(nonce.toHex)")

// Step2
let (terminalPubKey, terminalPrivKey) = domain.makeKeyPair()
let mappingKey = try await self.tag.sendPaceCommand(
records: [try TLV(
tag: 0x81,
publicKey: terminalPubKey
)],
tagExpected: 0x82
)
CardReaderNFC.logger().info("Mapping key \(mappingKey.value.toHex)")
guard let cardPubKey = try ECPublicKey(domain: domain, point: mappingKey.value)
else { throw IdCardInternalError.authenticationFailed }
let mappedPoint: Point
switch mappingType {
case .id_PACE_ECDH_IM_AES_CBC_CMAC_256:
let pcdNonce = try CardReaderNFC.random(count: nonce.count)
_ = try await self.tag.sendPaceCommand(records: [TLV(tag: 0x81, value: pcdNonce)], tagExpected: 0x82)
let psrn = try CardReaderNFC.pseudoRandomNumberMappingAES(sVal: nonce, tVal: pcdNonce, domain: domain)
mappedPoint = CardReaderNFC.pointEncodeIM(tVal: psrn, domain: domain)

// Mapping
let nonceS = BInt(magnitude: nonce)
let mappingBasePoint = ECPublicKey(privateKey: try ECPrivateKey(domain: domain, s: nonceS)) // S*G
// swiftlint:disable line_length
CardReaderNFC.logger().info("Card Key x: \(mappingBasePoint.w.x.asMagnitudeBytes().toHex, privacy: .public), y: \(mappingBasePoint.w.y.asMagnitudeBytes().toHex, privacy: .public)")
// swiftlint:enable line_length
let sharedSecretH = try domain.multiplyPoint(cardPubKey.w, terminalPrivKey.s)
// swiftlint:disable line_length
CardReaderNFC.logger().info("Shared Secret x: \(sharedSecretH.x.asMagnitudeBytes().toHex, privacy: .public), y: \(sharedSecretH.y.asMagnitudeBytes().toHex, privacy: .public)")
// swiftlint:enable line_length
let mappedPoint = try domain.addPoints(mappingBasePoint.w, sharedSecretH) // MAP G = (S*G) + H
case .id_PACE_ECDH_GM_AES_CBC_CMAC_256:
let (terminalPubKey, terminalPrivKey) = domain.makeKeyPair()
let mappingKey = try await self.tag.sendPaceCommand(
records: [try TLV(tag: 0x81, publicKey: terminalPubKey)],
tagExpected: 0x82)
CardReaderNFC.logger().info("Mapping key \(mappingKey.value.hex)")
let cardPubKey = try ECPublicKey(domain: domain, point: mappingKey.value)!

// Mapping
let nonceS = BInt(magnitude: nonce)
let mappingBasePoint = ECPublicKey(privateKey: try ECPrivateKey(domain: domain, s: nonceS)) // S*G
// swiftlint:disable line_length
CardReaderNFC.logger().info("Card Key x: \(mappingBasePoint.w.x.asMagnitudeBytes().hex), y: \(mappingBasePoint.w.y.asMagnitudeBytes().hex)")
// swiftlint:enable line_length
let sharedSecretH = try domain.multiplyPoint(cardPubKey.w, terminalPrivKey.s)
// swiftlint:disable line_length
CardReaderNFC.logger().info("Shared Secret x: \(sharedSecretH.x.asMagnitudeBytes().hex), y: \(sharedSecretH.y.asMagnitudeBytes().hex)")
// swiftlint:enable line_length
mappedPoint = try domain.addPoints(mappingBasePoint.w, sharedSecretH) // MAP G = (S*G) + H
}
// Ephemeral data
// swiftlint:disable line_length
CardReaderNFC.logger().info("Mapped point x: \(mappedPoint.x.asMagnitudeBytes().toHex, privacy: .public), y: \(mappedPoint.y.asMagnitudeBytes().toHex, privacy: .public)")
Expand Down Expand Up @@ -199,7 +205,11 @@ class CardReaderNFC: @unchecked CardReader, Loggable {
if let data = apdu.data, !data.isEmpty {
let ivValue = try AES.CBC(key: ksEnc).encrypt(SSC)
let encData = try AES.CBC(key: ksEnc, ivVal: ivValue).encrypt(data.addPadding())
return TLV(tag: 0x87, bytes: [0x01] + encData).data
if apdu.instructionCode & 0x01 == 0 {
return TLV(tag: 0x87, bytes: [0x01] + encData).data
} else {
return TLV(tag: 0x85, bytes: encData).data
}
} else {
return Data()
}
Expand All @@ -226,7 +236,7 @@ class CardReaderNFC: @unchecked CardReader, Loggable {
var tlvMac: TKTLVRecord?
for tlv in TLV.sequenceOfRecords(from: response) ?? [] {
switch tlv.tag {
case 0x87: tlvEnc = tlv
case 0x85, 0x87: tlvEnc = tlv
case 0x99: tlvRes = tlv
case 0x8E: tlvMac = tlv
default: CardReaderNFC.logger().info("Unknown tag")
Expand Down Expand Up @@ -273,13 +283,92 @@ class CardReaderNFC: @unchecked CardReader, Loggable {
return (.init(), UInt16(tlvRes.value[0], tlvRes.value[1]))
}
let ivValue = try AES.CBC(key: ksEnc).encrypt(SSC)
let responseData = try (try AES.CBC(key: ksEnc, ivVal: ivValue).decrypt(tlvEnc.value[1...])).removePadding()
let responseData = try (
try AES.CBC(key: ksEnc, ivVal: ivValue)
.decrypt(tlvEnc.tag == 0x85 ? tlvEnc.value : tlvEnc.value[1...]))
.removePadding()
CardReaderNFC.logger().info("Plain <: \(responseData.toHex) \(tlvRes.value.toHex)")
return (Bytes(responseData), UInt16(tlvRes.value[0], tlvRes.value[1]))
}

// MARK: - Utils

static private func pseudoRandomNumberMappingAES(
sVal: any AES.DataType,
tVal: any AES.DataType,
domain: Domain
) throws -> BInt {
let lVal = sVal.count * 8
let kVal = tVal.count * 8

let c0Val: Bytes
let c1Val: Bytes
switch lVal {
case 128:
c0Val = Bytes(hex: "a668892a7c41e3ca739f40b057d85904")!
c1Val = Bytes(hex: "a4e136ac725f738b01c1f60217c188ad")!
case 192, 256:
c0Val = Bytes(hex: "d463d65234124ef7897054986dca0a174e28df758cbaa03f240616414d5a1676")!
c1Val = Bytes(hex: "54bd7255f0aaf831bec3423fcf39d69b6cbf066677d0faae5aadd99df8e53517")!
default:
throw IdCardInternalError.authenticationFailed
}

let cipher = AES.CBC(key: tVal)
var key = try cipher.encrypt(sVal)

var xVal = Bytes()
var nVal = 0
while nVal * lVal < domain.p.bitWidth + 64 {
let cipher = AES.CBC(key: key.prefix(kVal / 8))
key = try cipher.encrypt(c0Val)
xVal += try cipher.encrypt(c1Val)
nVal += 1
}

return BInt(magnitude: xVal).mod(domain.p)
}

/**
* https://www.icao.int/Security/FAL/TRIP/Documents/TR%20-%20Supplemental%20Access%20Control%20V1.1.pdf
* A.2.1. Implementation for affine coordinates
*/
static private func pointEncodeIM(tVal: BInt, domain: Domain) -> Point {
let pVal = domain.p
let aVal = domain.a
let bVal = domain.b

// 1. α = -t^2 mod p
let alpha = (-(tVal ** 2)).mod(pVal)

// 2. X2 = -ba^-1 (1 + (α + α^2)^-1) mod p
// Hint = -b(1 + α + α^2)(a(α + α^2))^(p-2) mod p
let alphaPlusAlphaSqrt = alpha + alpha ** 2
let x2Val = ((-bVal * (1 + alphaPlusAlphaSqrt)) * (aVal * alphaPlusAlphaSqrt).expMod(pVal - 2, pVal)).mod(pVal)

// 3. X3 = α * X2 mod p
let x3Val = (alpha * x2Val).mod(pVal)

// 4. h2 = (X2)^3 + a * X2 + b mod p
let h2Val = (x2Val ** 3 + aVal * x2Val + bVal).mod(pVal)

// 5. h3 = (X3)^3 + a * X3 + b mod p
// Unused: let h3 = (X3 ** 3 + a * X3 + b).mod(p)

// 6. U = t^3 * h2 mod p
let UVal = (tVal ** 3 * h2Val).mod(pVal)

// 7. A = (h2)^(p - 1 - (p + 1) / 4) mod p
// Hint: modular exponentiation with exponent p-1-(p+1)/4.
let AVal = h2Val.expMod(pVal - BInt.ONE - (pVal + BInt.ONE) / BInt.FOUR, pVal)

// 8. A^2 * h2 mod p = 1 -> (x, y) = (X2, A h2 mod p)
// 9. (x, y) = (X3, A U mod p)
return (AVal ** 2 * h2Val).mod(pVal) == BInt.ONE ?
Point(x2Val, (AVal * h2Val).mod(pVal)) :
Point(x3Val, (AVal * UVal).mod(pVal))
}

static private func decryptNonce<T: AES.DataType>(CAN: String, encryptedNonce: T) throws -> Bytes {
let decryptionKey = KDF(key: Bytes(CAN.utf8), counter: 3)
let cipher = AES.CBC(key: decryptionKey)
Expand All @@ -298,7 +387,19 @@ class CardReaderNFC: @unchecked CardReader, Loggable {
initializedCount = Int(CC_SHA256_DIGEST_LENGTH)
}
}

static private func random(count: Int) throws -> Data {
var data = Data(count: count)
let result = data.withUnsafeMutableBytes { buffer in
SecRandomCopyBytes(kSecRandomDefault, count, buffer.baseAddress!)
}
if result != errSecSuccess {
throw IdCardInternalError.authenticationFailed
}
return data
}
}
// swiftlint:enable force_unwrapping

// MARK: - Extensions

Expand Down
11 changes: 6 additions & 5 deletions Modules/IdCardLib/Sources/IdCardLib/CardActions/Idemia.swift
Original file line number Diff line number Diff line change
Expand Up @@ -97,19 +97,20 @@ final class Idemia: CardCommandsInternal {

// MARK: - PIN & PUK Management

func readCodeTryCounterRecord(_ type: CodeType) async throws -> UInt8 {
func readCodeTryCounterRecord(_ type: CodeType) async throws -> (retryCount: UInt8, pinActive: Bool) {
_ = try await select(file: type.aid)
let ref = type.pinRef & ~0x80
let data = try await reader.sendAPDU(ins: 0xCB, p1Byte: 0x3F, p2Byte: 0xFF, data:
[0x4D, 0x08, 0x70, 0x06, 0xBF, 0x81, ref, 0x02, 0xA0, 0x80], leByte: 0x00)
if let info = TLV(from: data), info.tag == 0x70,
let tag = TLV(from: info.value), tag.tag == 0xBF8100 | UInt32(ref),
let a0value = TLV(from: tag.value), a0value.tag == 0xA0 {
for record in TLV.sequenceOfRecords(from: a0value.value) ?? [] where record.tag == 0x9B {
return record.value[0]
let a0Value = TLV(from: tag.value), a0Value.tag == 0xA0,
let records = TLV.sequenceOfRecords(from: a0Value.value) {
for record in records where record.tag == 0x9B {
return (record.value[0], true)
}
}
return 0
return (0, true)
}

func changeCode(_ type: CodeType, to code: SecureData, verifyCode: SecureData) async throws {
Expand Down
17 changes: 12 additions & 5 deletions Modules/IdCardLib/Sources/IdCardLib/CardActions/Thales.swift
Original file line number Diff line number Diff line change
Expand Up @@ -87,16 +87,23 @@ final class Thales: CardCommandsInternal {
}

// MARK: - PIN & PUK Management
func readCodeTryCounterRecord(_ type: CodeType) async throws -> UInt8 {
func readCodeTryCounterRecord(_ type: CodeType) async throws -> (retryCount: UInt8, pinActive: Bool) {
_ = try await select(file: Thales.kAID)
let data = try await reader.sendAPDU(ins: 0xCB, p1Byte: 0x00, p2Byte: 0xFF, data:
[0xA0, 0x03, 0x83, 0x01, type.pinRef], leByte: 0)
if let info = TLV(from: data), info.tag == 0xA0 {
for record in TLV.sequenceOfRecords(from: info.value) ?? [] where record.tag == 0xdf21 {
return record.value[0]
var retryCount: UInt8 = 0
var pinActive = true
if let info = TLV(from: data), info.tag == 0xA0,
let records = TLV.sequenceOfRecords(from: info.value) {
for record in records {
switch record.tag {
case 0xdf21: retryCount = record.value[0]
case 0xdf2f: pinActive = record.value[0] == 0x01
default: break
}
}
}
return 0
return (retryCount, pinActive)
}

func changeCode(_ type: CodeType, to code: SecureData, verifyCode: SecureData) async throws {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ public actor UsbReaderConnection: UsbReaderConnectionProtocol, Loggable {
}
}

public func readCodeTryCounterRecord(for codeType: CodeType) async throws -> UInt8 {
public func readCodeTryCounterRecord(for codeType: CodeType) async throws -> (retryCount: UInt8, pinActive: Bool) {
await ensureHandler()

UsbReaderConnection.logger().info("ID-CARD: Reading try counter with reader for \(codeType.name)")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public protocol UsbReaderConnectionProtocol: Actor {
func getPublicData() async throws -> CardInfo
func readAuthenticationCertificate() async throws -> Data
func readSignatureCertificate() async throws -> Data
func readCodeTryCounterRecord(for codeType: CodeType) async throws -> UInt8
func readCodeTryCounterRecord(for codeType: CodeType) async throws -> (retryCount: UInt8, pinActive: Bool)
func isPUKChangeable() async throws -> Bool
func changeCode(_ codeType: CodeType, to newCode: Data, verifyCode: Data) async throws
func unblockCode(_ codeType: CodeType, puk: Data, newCode: Data) async throws
Expand Down
2 changes: 1 addition & 1 deletion RIADigiDoc.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@
Domain/Model/Error/ReadCertAndSignError.swift,
Domain/Model/FileItem.swift,
Domain/Model/IdCard/IdCardData.swift,
Domain/Model/IdCard/RetryCount.swift,
Domain/Model/IdCard/PinResponse.swift,
Domain/Model/KeychainKey.swift,
"Domain/Model/My eID/MyEidDocumentStatus.swift",
"Domain/Model/My eID/MyEidPinCodeAction.swift",
Expand Down
2 changes: 1 addition & 1 deletion RIADigiDoc/Domain/Model/IdCard/IdCardData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,6 @@ public struct IdCardData: Sendable, Hashable {
public let publicData: CardInfo
public let authCertNotValidDate: String?
public let signCertNotValidDate: String?
public let retryCount: RetryCount
public let pinResponse: PinResponse
public let isPUKChangeable: Bool
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,11 @@

import Foundation

public struct RetryCount: Sendable, Hashable {
let pin1: UInt8
let pin2: UInt8
let puk: UInt8
public struct PinResponse: Sendable, Hashable {
let pin1RetryCount: UInt8
let pin1Active: Bool
let pin2RetryCount: UInt8
let pin2Active: Bool
let pukRetryCount: UInt8
let pukActive: Bool
}
2 changes: 1 addition & 1 deletion RIADigiDoc/Domain/Repository/IdCard/IdCardRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ actor IdCardRepository: IdCardRepositoryProtocol {
return try await idCardService.readSignatureCertificate()
}

func readCodeTryCounterRecord(for codeType: CodeType) async throws -> UInt8 {
func readCodeTryCounterRecord(for codeType: CodeType) async throws -> (retryCount: UInt8, pinActive: Bool) {
return try await idCardService.readCodeTryCounterRecord(for: codeType)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public protocol IdCardRepositoryProtocol: Sendable {
func getPublicData() async throws -> CardInfo
func readAuthenticationCertificate() async throws -> Data
func readSignatureCertificate() async throws -> Data
func readCodeTryCounterRecord(for codeType: CodeType) async throws -> UInt8
func readCodeTryCounterRecord(for codeType: CodeType) async throws -> (retryCount: UInt8, pinActive: Bool)
func isPUKChangeable() async throws -> Bool
func changeCode(_ codeType: CodeType, to newCode: Data, verifyCode: Data) async throws
func unblockCode(_ codeType: CodeType, puk: Data, newCode: Data) async throws
Expand Down
2 changes: 1 addition & 1 deletion RIADigiDoc/Domain/Service/IdCard/IdCardService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ actor IdCardService: IdCardServiceProtocol {
return try await usbReaderConnection.readSignatureCertificate()
}

func readCodeTryCounterRecord(for codeType: CodeType) async throws -> UInt8 {
func readCodeTryCounterRecord(for codeType: CodeType) async throws -> (retryCount: UInt8, pinActive: Bool) {
return try await usbReaderConnection.readCodeTryCounterRecord(for: codeType)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public protocol IdCardServiceProtocol: Sendable {
func getPublicData() async throws -> CardInfo
func readAuthenticationCertificate() async throws -> Data
func readSignatureCertificate() async throws -> Data
func readCodeTryCounterRecord(for codeType: CodeType) async throws -> UInt8
func readCodeTryCounterRecord(for codeType: CodeType) async throws -> (retryCount: UInt8, pinActive: Bool)
func isPUKChangeable() async throws -> Bool
func changeCode(_ codeType: CodeType, to newCode: Data, verifyCode: Data) async throws
func unblockCode(_ codeType: CodeType, puk: Data, newCode: Data) async throws
Expand Down
Loading