Skip to content
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
16 changes: 15 additions & 1 deletion Modules/CryptoLib/Sources/CryptoObjC/include/Encrypt.mm
Original file line number Diff line number Diff line change
Expand Up @@ -78,14 +78,28 @@ + (void)setProxy:(nonnull NSString *)host port:(NSInteger)port username:(nonnull
return @"UNKNOWN";
}

static inline NSString *BasenameFromPath(NSString *path) {
if (path == nil || path.length == 0) return @"<unknown>";
NSString *last = path.lastPathComponent;
if (last.length > 0) return last;

NSCharacterSet *seps = [NSCharacterSet characterSetWithCharactersInString:@"/\\"];
NSArray<NSString *> *parts = [path componentsSeparatedByCharactersInSet:seps];
for (NSInteger i = parts.count - 1; i >= 0; i--) {
if (parts[i].length > 0) return parts[i];
}
return path;
}

class ObjCLogger final : public libcdoc::ILogger {
public:
void LogMessage(libcdoc::ILogger::LogLevel level,
std::string_view file,
int line,
std::string_view message) override
{
NSString *nsFile = NSStringFromStringView(file);
NSString *nsFileFull = NSStringFromStringView(file);
NSString *nsFile = BasenameFromPath(nsFileFull);
NSString *nsMsg = NSStringFromStringView(message);
NSString *nsLvl = NSStringFromLogLevel(level);

Expand Down
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
174 changes: 136 additions & 38 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 @@ -72,9 +74,9 @@ class CardReaderNFC: @unchecked CardReader, Loggable {
init(_ tag: NFCISO7816Tag, CAN: String) async throws {
self.tag = SendableISO7816Tag(tag: tag)

CardReaderNFC.logger().info("Select CardAccess")
CardReaderNFC.logger().debug("Select CardAccess")
_ = try await self.tag.sendCommand(cls: 0x00, ins: 0xA4, p1Byte: 0x02, p2Byte: 0x0C, data: Data([0x01, 0x1C]))
CardReaderNFC.logger().info("Read CardAccess")
CardReaderNFC.logger().debug("Read CardAccess")
let data = try await self.tag.sendCommand(cls: 0x00, ins: 0xB0, p1Byte: 0x00, p2Byte: 0x00, leByte: 256)

guard let (mappingType, parameterId) = TLV.sequenceOfRecords(from: data)?
Expand Down Expand Up @@ -102,38 +104,42 @@ class CardReaderNFC: @unchecked CardReader, Loggable {

// Step1 - General Authentication
let nonceEnc = try await self.tag.sendPaceCommand(records: [], tagExpected: 0x80)
CardReaderNFC.logger().info("Challenge \(nonceEnc.value.toHex)")
CardReaderNFC.logger().debug("Challenge \(nonceEnc.value.toHex)")
let nonce = try CardReaderNFC.decryptNonce(CAN: CAN, encryptedNonce: nonceEnc.value)
CardReaderNFC.logger().info("Nonce \(nonce.toHex)")
CardReaderNFC.logger().debug("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().debug("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().debug("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().debug("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)")
CardReaderNFC.logger().debug("Mapped point x: \(mappedPoint.x.asMagnitudeBytes().toHex, privacy: .public), y: \(mappedPoint.y.asMagnitudeBytes().toHex, privacy: .public)")
// swiftlint:enable line_length
let mappedDomain = try Domain.instance(
name: domain.name + " Mapped",
Expand All @@ -153,17 +159,17 @@ class CardReaderNFC: @unchecked CardReader, Loggable {
)],
tagExpected: 0x84
)
CardReaderNFC.logger().info("Card Ephermal key \(ephemeralKey.value.toHex)")
CardReaderNFC.logger().debug("Card Ephermal key \(ephemeralKey.value.toHex)")
guard let ephemeralCardPubKey = try ECPublicKey(domain: mappedDomain, point: ephemeralKey.value)
else { throw IdCardInternalError.authenticationFailed }

// Derive shared secret and session keys
let sharedSecret = try terminalEphemeralPrivKey.sharedSecret(pubKey: ephemeralCardPubKey)
CardReaderNFC.logger().info("Shared secret \(sharedSecret.toHex)")
CardReaderNFC.logger().debug("Shared secret \(sharedSecret.toHex)")
ksEnc = CardReaderNFC.KDF(key: sharedSecret, counter: 1)
ksMac = CardReaderNFC.KDF(key: sharedSecret, counter: 2)
CardReaderNFC.logger().info("KS.Enc \(self.ksEnc.toHex)")
CardReaderNFC.logger().info("KS.Mac \(self.ksMac.toHex)")
CardReaderNFC.logger().debug("KS.Enc \(self.ksEnc.toHex)")
CardReaderNFC.logger().debug("KS.Mac \(self.ksMac.toHex)")

// Mutual authentication
let macCalc = try AES.CMAC(key: ksMac)
Expand All @@ -183,7 +189,7 @@ class CardReaderNFC: @unchecked CardReader, Loggable {
)],
tagExpected: 0x86
)
CardReaderNFC.logger().info("Mac response \(macValue.data.toHex)")
CardReaderNFC.logger().debug("Mac response \(macValue.data.toHex)")

// verify chip's MAC
let macResult = TLV(tag: 0x7f49, records: [
Expand All @@ -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,17 +236,17 @@ 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")
default: CardReaderNFC.logger().debug("Unknown tag")
}
}
return (tlvEnc, tlvRes, tlvMac)
}
// swiftlint:disable cyclomatic_complexity
func transmit(_ apduData: Bytes) async throws -> (responseData: Bytes, sw: UInt16) {
CardReaderNFC.logger().info("Plain >: \(apduData.toHex)")
CardReaderNFC.logger().debug("Plain >: \(apduData.toHex)")
guard let apdu = NFCISO7816APDU(data: Data(apduData)) else {
throw IdCardInternalError.invalidAPDU
}
Expand Down Expand Up @@ -298,20 +308,96 @@ class CardReaderNFC: @unchecked CardReader, Loggable {
throw IdCardInternalError.invalidMACValue
}
guard let tlvEnc else {
CardReaderNFC.logger().info("Plain <: \(tlvRes.value.toHex)")
CardReaderNFC.logger().debug("Plain <: \(tlvRes.value.toHex)")
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.tag == 0x85 ? tlvEnc.value : tlvEnc.value[1...]))
.removePadding()
CardReaderNFC.logger().info("Plain <: \(responseData.toHex) \(tlvRes.value.toHex)")
CardReaderNFC.logger().debug("Plain <: \(responseData.toHex) \(tlvRes.value.toHex)")
return (Bytes(responseData), UInt16(tlvRes.value[0], tlvRes.value[1]))
}
// swiftlint:enable cyclomatic_complexity

// 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 @@ -330,7 +416,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
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
public enum IdCardError: Error {
case wrongCAN,
wrongPIN(triesLeft: Int),
pinLocked,
invalidNewPIN,
sessionError,
cancelledByUser
Expand All @@ -36,6 +37,7 @@ public enum IdCardInternalError: Error {
invalidResponse(message: String),
swError(UInt16),
pinVerificationFailed,
pinLocked,
remainingPinRetryCount(Int),
invalidNewPin,
notSupportedCodeType,
Expand Down Expand Up @@ -91,6 +93,8 @@ public enum IdCardInternalError: Error {
return .invalidNewPIN
case .cancelledByUser:
return .cancelledByUser
case .pinLocked:
return .pinLocked
}
}
}
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
Loading