From 15bd3d29b47731c0f74f3da63553b3388175b3d7 Mon Sep 17 00:00:00 2001 From: Boriss Melikjan Date: Fri, 13 Feb 2026 14:01:46 +0200 Subject: [PATCH 01/11] MOPPIOS-1659 Show info when PIN is locked, detect Thales PIN2 not activated status. --- .../IdCardLib/CardActions/CardCommands.swift | 2 +- .../IdCardLib/CardActions/CardReader.swift | 2 +- .../IdCardLib/CardActions/CardReaderNFC.swift | 146 +++++++++++++++--- .../IdCardLib/CardActions/Idemia.swift | 11 +- .../IdCardLib/CardActions/Thales.swift | 17 +- .../Operations/UsbReaderConnection.swift | 2 +- .../UsbReaderConnectionProtocol.swift | 2 +- RIADigiDoc.xcodeproj/project.pbxproj | 2 +- .../Domain/Model/IdCard/IdCardData.swift | 2 +- .../{RetryCount.swift => PinResponse.swift} | 11 +- .../Repository/IdCard/IdCardRepository.swift | 2 +- .../IdCard/IdCardRepositoryProtocol.swift | 2 +- .../Domain/Service/IdCard/IdCardService.swift | 2 +- .../IdCard/IdCardServiceProtocol.swift | 2 +- .../Supporting files/Localizable.xcstrings | 34 ++++ .../My eID/MyEidPinsAndCertificatesView.swift | 29 ++++ .../UI/Component/My eID/MyEidView.swift | 23 ++- .../ViewModel/MyEid/MyEidViewModel.swift | 8 + .../MyEid/Shared/SharedMyEidSession.swift | 26 ++++ .../Shared/SharedMyEidSessionProtocol.swift | 2 + .../Signing/IdCard/IdCardViewModel.swift | 23 +-- 21 files changed, 285 insertions(+), 65 deletions(-) rename RIADigiDoc/Domain/Model/IdCard/{RetryCount.swift => PinResponse.swift} (79%) diff --git a/Modules/IdCardLib/Sources/IdCardLib/CardActions/CardCommands.swift b/Modules/IdCardLib/Sources/IdCardLib/CardActions/CardCommands.swift index 08662e9a..e0184188 100644 --- a/Modules/IdCardLib/Sources/IdCardLib/CardActions/CardCommands.swift +++ b/Modules/IdCardLib/Sources/IdCardLib/CardActions/CardCommands.swift @@ -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. diff --git a/Modules/IdCardLib/Sources/IdCardLib/CardActions/CardReader.swift b/Modules/IdCardLib/Sources/IdCardLib/CardActions/CardReader.swift index d6f4d299..fcc8a3aa 100644 --- a/Modules/IdCardLib/Sources/IdCardLib/CardActions/CardReader.swift +++ b/Modules/IdCardLib/Sources/IdCardLib/CardActions/CardReader.swift @@ -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)? = nil, leByte: UInt8? = nil) async throws -> Data { + data: (any Collection)? = nil, leByte: UInt8? = nil) async throws -> Data { var apdu: Bytes = [cls, ins, p1Byte, p2Byte] if let data { apdu.append(UInt8(data.count)) diff --git a/Modules/IdCardLib/Sources/IdCardLib/CardActions/CardReaderNFC.swift b/Modules/IdCardLib/Sources/IdCardLib/CardActions/CardReaderNFC.swift index 9cf39112..37553c87 100644 --- a/Modules/IdCardLib/Sources/IdCardLib/CardActions/CardReaderNFC.swift +++ b/Modules/IdCardLib/Sources/IdCardLib/CardActions/CardReaderNFC.swift @@ -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 { @@ -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 @@ -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)") @@ -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() } @@ -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") @@ -312,6 +322,82 @@ class CardReaderNFC: @unchecked CardReader, Loggable { // 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(CAN: String, encryptedNonce: T) throws -> Bytes { let decryptionKey = KDF(key: Bytes(CAN.utf8), counter: 3) let cipher = AES.CBC(key: decryptionKey) @@ -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 diff --git a/Modules/IdCardLib/Sources/IdCardLib/CardActions/Idemia.swift b/Modules/IdCardLib/Sources/IdCardLib/CardActions/Idemia.swift index dcd25174..4ee99f28 100644 --- a/Modules/IdCardLib/Sources/IdCardLib/CardActions/Idemia.swift +++ b/Modules/IdCardLib/Sources/IdCardLib/CardActions/Idemia.swift @@ -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 { diff --git a/Modules/IdCardLib/Sources/IdCardLib/CardActions/Thales.swift b/Modules/IdCardLib/Sources/IdCardLib/CardActions/Thales.swift index 4e09b5ce..e1214ce3 100644 --- a/Modules/IdCardLib/Sources/IdCardLib/CardActions/Thales.swift +++ b/Modules/IdCardLib/Sources/IdCardLib/CardActions/Thales.swift @@ -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 { diff --git a/Modules/IdCardLib/Sources/IdCardLib/Operations/UsbReaderConnection.swift b/Modules/IdCardLib/Sources/IdCardLib/Operations/UsbReaderConnection.swift index bf6b8ad7..fec6a299 100644 --- a/Modules/IdCardLib/Sources/IdCardLib/Operations/UsbReaderConnection.swift +++ b/Modules/IdCardLib/Sources/IdCardLib/Operations/UsbReaderConnection.swift @@ -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)") diff --git a/Modules/IdCardLib/Sources/IdCardLib/Operations/UsbReaderConnectionProtocol.swift b/Modules/IdCardLib/Sources/IdCardLib/Operations/UsbReaderConnectionProtocol.swift index 5c535e07..2feb32bf 100644 --- a/Modules/IdCardLib/Sources/IdCardLib/Operations/UsbReaderConnectionProtocol.swift +++ b/Modules/IdCardLib/Sources/IdCardLib/Operations/UsbReaderConnectionProtocol.swift @@ -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 diff --git a/RIADigiDoc.xcodeproj/project.pbxproj b/RIADigiDoc.xcodeproj/project.pbxproj index 3552d64e..67b8d856 100644 --- a/RIADigiDoc.xcodeproj/project.pbxproj +++ b/RIADigiDoc.xcodeproj/project.pbxproj @@ -169,7 +169,7 @@ Domain/Model/Error/NFC/UnblockPINError.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", diff --git a/RIADigiDoc/Domain/Model/IdCard/IdCardData.swift b/RIADigiDoc/Domain/Model/IdCard/IdCardData.swift index d0b5812b..f7784fa9 100644 --- a/RIADigiDoc/Domain/Model/IdCard/IdCardData.swift +++ b/RIADigiDoc/Domain/Model/IdCard/IdCardData.swift @@ -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 } diff --git a/RIADigiDoc/Domain/Model/IdCard/RetryCount.swift b/RIADigiDoc/Domain/Model/IdCard/PinResponse.swift similarity index 79% rename from RIADigiDoc/Domain/Model/IdCard/RetryCount.swift rename to RIADigiDoc/Domain/Model/IdCard/PinResponse.swift index 53023b20..ae6e859a 100644 --- a/RIADigiDoc/Domain/Model/IdCard/RetryCount.swift +++ b/RIADigiDoc/Domain/Model/IdCard/PinResponse.swift @@ -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 } diff --git a/RIADigiDoc/Domain/Repository/IdCard/IdCardRepository.swift b/RIADigiDoc/Domain/Repository/IdCard/IdCardRepository.swift index 116fe7a0..b618bf11 100644 --- a/RIADigiDoc/Domain/Repository/IdCard/IdCardRepository.swift +++ b/RIADigiDoc/Domain/Repository/IdCard/IdCardRepository.swift @@ -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) } diff --git a/RIADigiDoc/Domain/Repository/IdCard/IdCardRepositoryProtocol.swift b/RIADigiDoc/Domain/Repository/IdCard/IdCardRepositoryProtocol.swift index 910d2457..57dffb5c 100644 --- a/RIADigiDoc/Domain/Repository/IdCard/IdCardRepositoryProtocol.swift +++ b/RIADigiDoc/Domain/Repository/IdCard/IdCardRepositoryProtocol.swift @@ -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 diff --git a/RIADigiDoc/Domain/Service/IdCard/IdCardService.swift b/RIADigiDoc/Domain/Service/IdCard/IdCardService.swift index e4576188..166b64f3 100644 --- a/RIADigiDoc/Domain/Service/IdCard/IdCardService.swift +++ b/RIADigiDoc/Domain/Service/IdCard/IdCardService.swift @@ -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) } diff --git a/RIADigiDoc/Domain/Service/IdCard/IdCardServiceProtocol.swift b/RIADigiDoc/Domain/Service/IdCard/IdCardServiceProtocol.swift index a4ebf28f..862868a5 100644 --- a/RIADigiDoc/Domain/Service/IdCard/IdCardServiceProtocol.swift +++ b/RIADigiDoc/Domain/Service/IdCard/IdCardServiceProtocol.swift @@ -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 diff --git a/RIADigiDoc/Supporting files/Localizable.xcstrings b/RIADigiDoc/Supporting files/Localizable.xcstrings index cc418cdd..1bf7f0f7 100644 --- a/RIADigiDoc/Supporting files/Localizable.xcstrings +++ b/RIADigiDoc/Supporting files/Localizable.xcstrings @@ -5754,6 +5754,40 @@ } } }, + "PIN2 locked" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Signing with the ID-card isn't possible yet. PIN2 code must be changed in order to sign. " + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Selle ID-kaardiga allkirjastamine ei ole veel võimalik. Allkirjastamiseks tuleb PIN2-koodi muuta." + } + } + } + }, + "PIN2 locked URL" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "https://www.id.ee/en/article/changing-id-card-pin-codes-and-puk-code/" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "https://www.id.ee/artikkel/id-kaardi-pin-ja-puk-koodide-muutmine/" + } + } + } + }, "PINs and certificates" : { "comment" : "My eID tab title", "extractionState" : "manual", diff --git a/RIADigiDoc/UI/Component/My eID/MyEidPinsAndCertificatesView.swift b/RIADigiDoc/UI/Component/My eID/MyEidPinsAndCertificatesView.swift index b357e14c..06848d10 100644 --- a/RIADigiDoc/UI/Component/My eID/MyEidPinsAndCertificatesView.swift +++ b/RIADigiDoc/UI/Component/My eID/MyEidPinsAndCertificatesView.swift @@ -29,11 +29,24 @@ struct MyEidPinsAndCertificatesView: View { @Binding var isPin1Blocked: Bool @Binding var isPin2Blocked: Bool @Binding var isPukBlocked: Bool + @Binding var isPin2Activated: Bool @Binding var pinChangeVariant: PinChangeVariant? var authCertValidTo: String var signCertValidTo: String var isPUKChangeable: Bool + private var pin2LockedMessage: String { + languageSettings.localized( + "PIN2 locked" + ) + } + + private var pin2LockedUrl: String { + languageSettings.localized( + "PIN2 locked URL" + ) + } + private var pukBlockedMessage: String { languageSettings.localized( isPUKChangeable ? "PUK blocked" : "PUK blocked Thales" @@ -63,6 +76,7 @@ struct MyEidPinsAndCertificatesView: View { isPin1Blocked: Binding, isPin2Blocked: Binding, isPukBlocked: Binding, + isPin2Activated: Binding, pinChangeVariant: Binding = .constant(nil), authCertValidTo: String, signCertValidTo: String, @@ -71,6 +85,7 @@ struct MyEidPinsAndCertificatesView: View { self._isPin1Blocked = isPin1Blocked self._isPin2Blocked = isPin2Blocked self._isPukBlocked = isPukBlocked + self._isPin2Activated = isPin2Activated self._pinChangeVariant = pinChangeVariant self.authCertValidTo = authCertValidTo self.signCertValidTo = signCertValidTo @@ -139,6 +154,20 @@ struct MyEidPinsAndCertificatesView: View { .foregroundStyle(theme.error) .padding(.vertical, Dimensions.Padding.XSPadding) } + + if !isPin2Activated { + VStack(alignment: .leading) { + Text(verbatim: pin2LockedMessage) + .font(typography.bodySmall) + .foregroundStyle(theme.error) + .padding(.vertical, Dimensions.Padding.XSPadding) + + if let pin2LockedInfoUrl = URL(string: pin2LockedUrl) { + additionalInformationLink(url: pin2LockedInfoUrl) + .padding(.vertical, Dimensions.Padding.XSPadding) + } + } + } } .padding(.bottom, Dimensions.Padding.SPadding) diff --git a/RIADigiDoc/UI/Component/My eID/MyEidView.swift b/RIADigiDoc/UI/Component/My eID/MyEidView.swift index fe1709be..64db6355 100644 --- a/RIADigiDoc/UI/Component/My eID/MyEidView.swift +++ b/RIADigiDoc/UI/Component/My eID/MyEidView.swift @@ -85,9 +85,13 @@ struct MyEidView: View { self.idCardData = idCardData self.actionMethod = actionMethod - viewModel.setIsPinBlocked(.pin1, isBlocked: idCardData.retryCount.pin1 == 0) - viewModel.setIsPinBlocked(.pin2, isBlocked: idCardData.retryCount.pin2 == 0) - viewModel.setIsPinBlocked(.puk, isBlocked: idCardData.retryCount.puk == 0) + viewModel.setIsPinLocked(.pin1, isLocked: idCardData.pinResponse.pin1Active != true) + viewModel.setIsPinLocked(.pin2, isLocked: idCardData.pinResponse.pin2Active != true) + viewModel.setIsPinLocked(.puk, isLocked: idCardData.pinResponse.pukActive != true) + + viewModel.setIsPinBlocked(.pin1, isBlocked: idCardData.pinResponse.pin1RetryCount == 0) + viewModel.setIsPinBlocked(.pin2, isBlocked: idCardData.pinResponse.pin2RetryCount == 0) + viewModel.setIsPinBlocked(.puk, isBlocked: idCardData.pinResponse.pukRetryCount == 0) } var body: some View { @@ -129,6 +133,7 @@ struct MyEidView: View { isPin1Blocked: $isPin1Blocked, isPin2Blocked: $isPin2Blocked, isPukBlocked: $isPukBlocked, + isPin2Activated: $isPin2Activated, pinChangeVariant: $pinChangeVariant, authCertValidTo: idCardData.authCertNotValidDate ?? "", signCertValidTo: idCardData.signCertNotValidDate ?? "", @@ -218,6 +223,7 @@ struct MyEidView: View { } } .onAppear { + isPin2Activated = !viewModel.getIsPinLocked(for: .pin2) isPin1Blocked = viewModel.getIsPinBlocked(for: .pin1) isPin2Blocked = viewModel.getIsPinBlocked(for: .pin2) isPukBlocked = viewModel.getIsPinBlocked(for: .puk) @@ -300,10 +306,13 @@ struct MyEidView: View { ), authCertNotValidDate: nil, signCertNotValidDate: nil, - retryCount: RetryCount( - pin1: 3, - pin2: 3, - puk: 3 + pinResponse: PinResponse( + pin1RetryCount: 3, + pin1Active: true, + pin2RetryCount: 3, + pin2Active: true, + pukRetryCount: 3, + pukActive: true, ), isPUKChangeable: true ), diff --git a/RIADigiDoc/ViewModel/MyEid/MyEidViewModel.swift b/RIADigiDoc/ViewModel/MyEid/MyEidViewModel.swift index 885a4412..430e2317 100644 --- a/RIADigiDoc/ViewModel/MyEid/MyEidViewModel.swift +++ b/RIADigiDoc/ViewModel/MyEid/MyEidViewModel.swift @@ -62,6 +62,14 @@ class MyEidViewModel: MyEidViewModelProtocol, Loggable { .date } + public func setIsPinLocked(_ codeType: CodeType, isLocked: Bool) { + sharedMyEidSession.setIsPinLocked(codeType, isLocked: isLocked) + } + + public func getIsPinLocked(for codeType: CodeType) -> Bool { + return sharedMyEidSession.getIsPinLocked(for: codeType) + } + public func setIsPinBlocked(_ codeType: CodeType, isBlocked: Bool) { sharedMyEidSession.setIsPinBlocked(codeType, isBlocked: isBlocked) } diff --git a/RIADigiDoc/ViewModel/MyEid/Shared/SharedMyEidSession.swift b/RIADigiDoc/ViewModel/MyEid/Shared/SharedMyEidSession.swift index 0ce6d1ed..72eb1111 100644 --- a/RIADigiDoc/ViewModel/MyEid/Shared/SharedMyEidSession.swift +++ b/RIADigiDoc/ViewModel/MyEid/Shared/SharedMyEidSession.swift @@ -32,6 +32,10 @@ final class SharedMyEidSession: SharedMyEidSessionProtocol { private var canNumber = "" + private var isPin1Locked = false + private var isPin2Locked = false + private var isPukLocked = false + private let idCardRepository: IdCardRepositoryProtocol private var task: Task? @@ -40,6 +44,28 @@ final class SharedMyEidSession: SharedMyEidSessionProtocol { startStatusStream() } + public func setIsPinLocked(_ codeType: CodeType, isLocked: Bool) { + switch codeType { + case .pin1: + self.isPin1Locked = isLocked + case .pin2: + self.isPin2Locked = isLocked + case .puk: + self.isPukLocked = isLocked + } + } + + public func getIsPinLocked(for codeType: CodeType) -> Bool { + switch codeType { + case .pin1: + return self.isPin1Locked + case .pin2: + return self.isPin2Locked + case .puk: + return self.isPukLocked + } + } + public func setIsPinBlocked(_ codeType: CodeType, isBlocked: Bool) { switch codeType { case .pin1: diff --git a/RIADigiDoc/ViewModel/MyEid/Shared/SharedMyEidSessionProtocol.swift b/RIADigiDoc/ViewModel/MyEid/Shared/SharedMyEidSessionProtocol.swift index bb713de4..2c42e46d 100644 --- a/RIADigiDoc/ViewModel/MyEid/Shared/SharedMyEidSessionProtocol.swift +++ b/RIADigiDoc/ViewModel/MyEid/Shared/SharedMyEidSessionProtocol.swift @@ -24,6 +24,8 @@ import IdCardLib @MainActor public protocol SharedMyEidSessionProtocol: Sendable { var usbReaderStatus: UsbReaderStatus { get } + func setIsPinLocked(_ codeType: CodeType, isLocked: Bool) + func getIsPinLocked(for codeType: CodeType) -> Bool func setIsPinBlocked(_ codeType: CodeType, isBlocked: Bool) func getIsPinBlocked(for codeType: CodeType) -> Bool func stopStatusStream() diff --git a/RIADigiDoc/ViewModel/Signing/IdCard/IdCardViewModel.swift b/RIADigiDoc/ViewModel/Signing/IdCard/IdCardViewModel.swift index 5db92b8e..8981b75d 100644 --- a/RIADigiDoc/ViewModel/Signing/IdCard/IdCardViewModel.swift +++ b/RIADigiDoc/ViewModel/Signing/IdCard/IdCardViewModel.swift @@ -175,14 +175,14 @@ class IdCardViewModel: IdCardViewModelProtocol, Loggable { let publicData = try await getPublicData() let authCertNotValidDate = try await readAuthenticationCertificateNotValidDate() let signCertNotValidDate = try await readSignatureCertificateNotValidDate() - let retryCount = try await readCodeTryCounterRecord() + let pinResponse = try await readCodeTryCounterRecord() let isPUKChangeable = try await isPukChangeable() return IdCardData( publicData: publicData, authCertNotValidDate: authCertNotValidDate, signCertNotValidDate: signCertNotValidDate, - retryCount: retryCount, + pinResponse: pinResponse, isPUKChangeable: isPUKChangeable ) } catch { @@ -247,18 +247,21 @@ class IdCardViewModel: IdCardViewModelProtocol, Loggable { return try getNotValidDate(from: signCert) } - private func readCodeTryCounterRecord() async throws -> RetryCount { + private func readCodeTryCounterRecord() async throws -> PinResponse { IdCardViewModel.logger().info( "ID-CARD: Reading retry counter record from ID-card with reader" ) - let pin1RetryCount = try await idCardRepository.readCodeTryCounterRecord(for: .pin1) - let pin2RetryCount = try await idCardRepository.readCodeTryCounterRecord(for: .pin2) - let pukRetryCount = try await idCardRepository.readCodeTryCounterRecord(for: .puk) - return RetryCount( - pin1: pin1RetryCount, - pin2: pin2RetryCount, - puk: pukRetryCount + let pin1Response = try await idCardRepository.readCodeTryCounterRecord(for: .pin1) + let pin2Response = try await idCardRepository.readCodeTryCounterRecord(for: .pin2) + let pukResponse = try await idCardRepository.readCodeTryCounterRecord(for: .puk) + return PinResponse( + pin1RetryCount: pin1Response.retryCount, + pin1Active: pin1Response.pinActive, + pin2RetryCount: pin2Response.retryCount, + pin2Active: pin2Response.pinActive, + pukRetryCount: pukResponse.retryCount, + pukActive: pukResponse.pinActive, ) } From 2f07ed8b1159a7b7d3fc837d831e3a414ac11771 Mon Sep 17 00:00:00 2001 From: Boriss Melikjan Date: Mon, 16 Feb 2026 22:21:53 +0200 Subject: [PATCH 02/11] MOPPIOS-1659 Update with latest changes from main branch. --- RIADigiDoc/Domain/Model/NFC/NFCCardData.swift | 2 +- .../Domain/NFC/OperationReadCardData.swift | 32 +++++++++++-------- .../ViewModel/Signing/NFC/NFCViewModel.swift | 2 +- 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/RIADigiDoc/Domain/Model/NFC/NFCCardData.swift b/RIADigiDoc/Domain/Model/NFC/NFCCardData.swift index 912eaf36..a94b83e3 100644 --- a/RIADigiDoc/Domain/Model/NFC/NFCCardData.swift +++ b/RIADigiDoc/Domain/Model/NFC/NFCCardData.swift @@ -24,6 +24,6 @@ public struct NFCCardData: Sendable { let publicData: CardInfo let authenticationCertificate: Data? let signatureCertificate: Data? - let retryCount: RetryCount + let pinResponse: PinResponse let isPUKChangable: Bool } diff --git a/RIADigiDoc/Domain/NFC/OperationReadCardData.swift b/RIADigiDoc/Domain/NFC/OperationReadCardData.swift index ce2249a2..4ae592ba 100644 --- a/RIADigiDoc/Domain/NFC/OperationReadCardData.swift +++ b/RIADigiDoc/Domain/NFC/OperationReadCardData.swift @@ -78,19 +78,23 @@ final public class OperationReadCardData: NFCOperationBase { updateAlertMessage(step: 4) OperationReadCardData.logger().info("Reading PIN retry counts...") - let pin1Count = try await cardCommands.readCodeTryCounterRecord(.pin1) - OperationReadCardData.logger().info("PIN1 retry count: \(pin1Count)") - - let pin2Count = try await cardCommands.readCodeTryCounterRecord(.pin2) - OperationReadCardData.logger().info("PIN2 retry count: \(pin2Count)") - - let pukCount = try await cardCommands.readCodeTryCounterRecord(.puk) - OperationReadCardData.logger().info("PUK retry count: \(pukCount)") - - let retryCount = RetryCount( - pin1: pin1Count, - pin2: pin2Count, - puk: pukCount + 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, ) OperationReadCardData.logger().info("NFC: reading can change PUK") @@ -101,7 +105,7 @@ final public class OperationReadCardData: NFCOperationBase { publicData: cardInfo, authenticationCertificate: authenticationCertificate, signatureCertificate: signatureCertificate, - retryCount: retryCount, + pinResponse: pinResponse, isPUKChangable: canChangePUK ) diff --git a/RIADigiDoc/ViewModel/Signing/NFC/NFCViewModel.swift b/RIADigiDoc/ViewModel/Signing/NFC/NFCViewModel.swift index cb827138..5d8e734b 100644 --- a/RIADigiDoc/ViewModel/Signing/NFC/NFCViewModel.swift +++ b/RIADigiDoc/ViewModel/Signing/NFC/NFCViewModel.swift @@ -475,7 +475,7 @@ class NFCViewModel: NFCViewModelProtocol, Loggable { publicData: nfcCardData.publicData, authCertNotValidDate: authCertNotValidDate, signCertNotValidDate: signCertNotValidDate, - retryCount: nfcCardData.retryCount, + pinResponse: nfcCardData.pinResponse, isPUKChangeable: nfcCardData.isPUKChangable ) } catch { From cf6eae8348e77227745d9b967a3e2fc25241787b Mon Sep 17 00:00:00 2001 From: Boriss Melikjan Date: Tue, 17 Feb 2026 12:00:40 +0200 Subject: [PATCH 03/11] MOPPIOS-1659 Check Thales PIN2 locked status upon signing. --- .../IdCardLib/CardActions/CardReaderNFC.swift | 34 +++++++++---------- .../IdCardLib/CardActions/IDCardError.swift | 4 +++ RIADigiDoc/Domain/NFC/OperationDecrypt.swift | 7 ++++ .../Domain/NFC/OperationReadCertAndSign.swift | 9 +++++ .../My eID/MyEidPinsAndCertificatesView.swift | 3 ++ .../ViewModel/Signing/NFC/NFCViewModel.swift | 4 +++ 6 files changed, 44 insertions(+), 17 deletions(-) diff --git a/Modules/IdCardLib/Sources/IdCardLib/CardActions/CardReaderNFC.swift b/Modules/IdCardLib/Sources/IdCardLib/CardActions/CardReaderNFC.swift index 37553c87..68ace565 100644 --- a/Modules/IdCardLib/Sources/IdCardLib/CardActions/CardReaderNFC.swift +++ b/Modules/IdCardLib/Sources/IdCardLib/CardActions/CardReaderNFC.swift @@ -74,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)? @@ -104,9 +104,9 @@ 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 mappedPoint: Point @@ -122,24 +122,24 @@ class CardReaderNFC: @unchecked CardReader, Loggable { let mappingKey = try await self.tag.sendPaceCommand( records: [try TLV(tag: 0x81, publicKey: terminalPubKey)], tagExpected: 0x82) - CardReaderNFC.logger().info("Mapping key \(mappingKey.value.hex)") + 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().info("Card Key x: \(mappingBasePoint.w.x.asMagnitudeBytes().hex), y: \(mappingBasePoint.w.y.asMagnitudeBytes().hex)") + 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().info("Shared Secret x: \(sharedSecretH.x.asMagnitudeBytes().hex), y: \(sharedSecretH.y.asMagnitudeBytes().hex)") + 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", @@ -159,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) @@ -189,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: [ @@ -239,14 +239,14 @@ class CardReaderNFC: @unchecked CardReader, Loggable { 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 } @@ -308,14 +308,14 @@ 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 diff --git a/Modules/IdCardLib/Sources/IdCardLib/CardActions/IDCardError.swift b/Modules/IdCardLib/Sources/IdCardLib/CardActions/IDCardError.swift index 3e5179e6..6cf8691f 100644 --- a/Modules/IdCardLib/Sources/IdCardLib/CardActions/IDCardError.swift +++ b/Modules/IdCardLib/Sources/IdCardLib/CardActions/IDCardError.swift @@ -20,6 +20,7 @@ public enum IdCardError: Error { case wrongCAN, wrongPIN(triesLeft: Int), + pinLocked, invalidNewPIN, sessionError, cancelledByUser @@ -36,6 +37,7 @@ public enum IdCardInternalError: Error { invalidResponse(message: String), swError(UInt16), pinVerificationFailed, + pinLocked, remainingPinRetryCount(Int), invalidNewPin, notSupportedCodeType, @@ -91,6 +93,8 @@ public enum IdCardInternalError: Error { return .invalidNewPIN case .cancelledByUser: return .cancelledByUser + case .pinLocked: + return .pinLocked } } } diff --git a/RIADigiDoc/Domain/NFC/OperationDecrypt.swift b/RIADigiDoc/Domain/NFC/OperationDecrypt.swift index fc3eed18..91c8d1ec 100644 --- a/RIADigiDoc/Domain/NFC/OperationDecrypt.swift +++ b/RIADigiDoc/Domain/NFC/OperationDecrypt.swift @@ -106,6 +106,13 @@ public class OperationDecrypt: NFCOperationBase { updateAlertMessage(step: 2) let cardCommands = try await connection.getCardCommands(session, tag: tag, CAN: canNumber) updateAlertMessage(step: 3) + + let (retryCount, pinActive) = try await cardCommands.readCodeTryCounterRecord(.pin1) + + if retryCount == 0 { + throw IdCardInternalError.remainingPinRetryCount(Int(retryCount)) + } + let cert = try await cardCommands.readAuthenticationCertificate() updateAlertMessage(step: 4) let decryptedContainer = try await CryptoContainer.decrypt( diff --git a/RIADigiDoc/Domain/NFC/OperationReadCertAndSign.swift b/RIADigiDoc/Domain/NFC/OperationReadCertAndSign.swift index 5d3c8d88..8120af52 100644 --- a/RIADigiDoc/Domain/NFC/OperationReadCertAndSign.swift +++ b/RIADigiDoc/Domain/NFC/OperationReadCertAndSign.swift @@ -123,6 +123,15 @@ public class OperationReadCertAndSign: NFCOperationBase { let cardCommands = try await connection.getCardCommands(session, tag: tag, CAN: canNumber) updateAlertMessage(step: 3) + let (retryCount, pinActive) = try await cardCommands.readCodeTryCounterRecord(.pin2) + + if retryCount == 0 { + throw IdCardInternalError.remainingPinRetryCount(Int(retryCount)) + } + if !pinActive { + throw IdCardInternalError.pinLocked + } + let cert = try await cardCommands.readSignatureCertificate() let hashToSign = try await signedContainer.prepareSignature( cert: cert, diff --git a/RIADigiDoc/UI/Component/My eID/MyEidPinsAndCertificatesView.swift b/RIADigiDoc/UI/Component/My eID/MyEidPinsAndCertificatesView.swift index 06848d10..2bcf2572 100644 --- a/RIADigiDoc/UI/Component/My eID/MyEidPinsAndCertificatesView.swift +++ b/RIADigiDoc/UI/Component/My eID/MyEidPinsAndCertificatesView.swift @@ -170,6 +170,9 @@ struct MyEidPinsAndCertificatesView: View { } } .padding(.bottom, Dimensions.Padding.SPadding) + .onAppear { + + } VStack { MyEidCertificateCardView( diff --git a/RIADigiDoc/ViewModel/Signing/NFC/NFCViewModel.swift b/RIADigiDoc/ViewModel/Signing/NFC/NFCViewModel.swift index 5d8e734b..4a46f1f0 100644 --- a/RIADigiDoc/ViewModel/Signing/NFC/NFCViewModel.swift +++ b/RIADigiDoc/ViewModel/Signing/NFC/NFCViewModel.swift @@ -252,6 +252,10 @@ class NFCViewModel: NFCViewModelProtocol, Loggable { case .cancelledByUser: nfcErrorKey = nil nfcErrorExtraArguments = [] + case .pinLocked: + showNfcAlertMessage = true + nfcAlertMessageKey = "PIN2 locked" + nfcAlertMessageUrl = "PIN2 locked URL" case .wrongCAN: nfcErrorKey = "Wrong CAN" nfcErrorExtraArguments = [] From c77d971e87f4230e824a22287afd99ba2138514c Mon Sep 17 00:00:00 2001 From: Boriss Melikjan Date: Tue, 17 Feb 2026 12:07:37 +0200 Subject: [PATCH 04/11] MOPPIOS-1659 Check Thales PIN2 locked status upon signing with USB card reader. --- RIADigiDoc/Domain/NFC/OperationDecrypt.swift | 2 +- .../Domain/NFC/OperationReadCertAndSign.swift | 1 + .../Signing/IdCard/IdCardViewModel.swift | 21 +++++++++++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/RIADigiDoc/Domain/NFC/OperationDecrypt.swift b/RIADigiDoc/Domain/NFC/OperationDecrypt.swift index 91c8d1ec..cb3ce5dc 100644 --- a/RIADigiDoc/Domain/NFC/OperationDecrypt.swift +++ b/RIADigiDoc/Domain/NFC/OperationDecrypt.swift @@ -107,7 +107,7 @@ public class OperationDecrypt: NFCOperationBase { let cardCommands = try await connection.getCardCommands(session, tag: tag, CAN: canNumber) updateAlertMessage(step: 3) - let (retryCount, pinActive) = try await cardCommands.readCodeTryCounterRecord(.pin1) + let (retryCount, _) = try await cardCommands.readCodeTryCounterRecord(.pin1) if retryCount == 0 { throw IdCardInternalError.remainingPinRetryCount(Int(retryCount)) diff --git a/RIADigiDoc/Domain/NFC/OperationReadCertAndSign.swift b/RIADigiDoc/Domain/NFC/OperationReadCertAndSign.swift index 8120af52..a75ec501 100644 --- a/RIADigiDoc/Domain/NFC/OperationReadCertAndSign.swift +++ b/RIADigiDoc/Domain/NFC/OperationReadCertAndSign.swift @@ -123,6 +123,7 @@ public class OperationReadCertAndSign: NFCOperationBase { let cardCommands = try await connection.getCardCommands(session, tag: tag, CAN: canNumber) updateAlertMessage(step: 3) + let (retryCount, pinActive) = try await cardCommands.readCodeTryCounterRecord(.pin2) if retryCount == 0 { diff --git a/RIADigiDoc/ViewModel/Signing/IdCard/IdCardViewModel.swift b/RIADigiDoc/ViewModel/Signing/IdCard/IdCardViewModel.swift index 8981b75d..c7174547 100644 --- a/RIADigiDoc/ViewModel/Signing/IdCard/IdCardViewModel.swift +++ b/RIADigiDoc/ViewModel/Signing/IdCard/IdCardViewModel.swift @@ -92,6 +92,13 @@ class IdCardViewModel: IdCardViewModelProtocol, Loggable { let pinSecureData = SecureData(Array(pin1.utf8)) let cardCommands = try await idCardRepository.getCardHandler() + + let (retryCount, _) = try await idCardRepository.readCodeTryCounterRecord(for: .pin1) + + if retryCount == 0 { + throw IdCardInternalError.remainingPinRetryCount(Int(retryCount)) + } + let authCertData = try await idCardRepository.readAuthenticationCertificate() let container = try await CryptoContainer.decrypt( containerFile: containerFile, @@ -126,6 +133,16 @@ class IdCardViewModel: IdCardViewModelProtocol, Loggable { do { let containerFile = await signedContainer.getRawContainerFile() ?? URL(fileURLWithPath: "") let pinSecureData = SecureData(Array(pin2.utf8)) + + let (retryCount, pinActive) = try await idCardRepository.readCodeTryCounterRecord(for: .pin2) + + if retryCount == 0 { + throw IdCardInternalError.remainingPinRetryCount(Int(retryCount)) + } + if !pinActive { + throw IdCardInternalError.pinLocked + } + let signatureCertificate = try await idCardRepository.readSignatureCertificate() IdCardViewModel.logger().info("ID-CARD: Getting language") @@ -290,6 +307,10 @@ class IdCardViewModel: IdCardViewModelProtocol, Loggable { errorMessage = nil errorExtraArguments = [] shouldDismissForError = true + case .pinLocked: + showIdCardAlertMessage = true + idCardAlertMessageKey = "PIN2 locked" + idCardAlertMessageUrl = "PIN2 locked URL" case .wrongPIN(let triesLeft): if triesLeft > 1 { errorMessage = "PIN verification error multiple" From 74824d4bd6c7578ae6b963aa37d5e5963646850c Mon Sep 17 00:00:00 2001 From: Boriss Melikjan Date: Tue, 17 Feb 2026 16:37:06 +0200 Subject: [PATCH 05/11] MOPPIOS-1659 Fix for checking PIN blocked and locked status before showing PIN screen (USB card reader). --- .../Container/Signing/IdCard/IdCardView.swift | 6 +-- .../My eID/MyEidPinsAndCertificatesView.swift | 22 +++++++--- .../Signing/IdCard/IdCardViewModel.swift | 42 ++++++++++++++++++- .../IdCard/IdCardViewModelProtocol.swift | 3 +- 4 files changed, 62 insertions(+), 11 deletions(-) diff --git a/RIADigiDoc/UI/Component/Container/Signing/IdCard/IdCardView.swift b/RIADigiDoc/UI/Component/Container/Signing/IdCard/IdCardView.swift index 7191cf21..43980c81 100644 --- a/RIADigiDoc/UI/Component/Container/Signing/IdCard/IdCardView.swift +++ b/RIADigiDoc/UI/Component/Container/Signing/IdCard/IdCardView.swift @@ -300,7 +300,7 @@ struct IdCardView: View { return } - idCardData = await viewModel.getIdCardData() + idCardData = await viewModel.getIdCardData(for: .pin1) guard idCardData != nil else { await handleCardError() return @@ -320,7 +320,7 @@ struct IdCardView: View { return } - idCardData = await viewModel.getIdCardData() + idCardData = await viewModel.getIdCardData(for: .pin2) guard idCardData != nil else { await handleCardError() return @@ -334,7 +334,7 @@ struct IdCardView: View { case .myeid: guard newValue == .sCardConnected else { return } - let cardData = await viewModel.getIdCardData() + let cardData = await viewModel.getIdCardDataMyEid() guard let cardData else { await handleCardError() return diff --git a/RIADigiDoc/UI/Component/My eID/MyEidPinsAndCertificatesView.swift b/RIADigiDoc/UI/Component/My eID/MyEidPinsAndCertificatesView.swift index 2bcf2572..ecf26264 100644 --- a/RIADigiDoc/UI/Component/My eID/MyEidPinsAndCertificatesView.swift +++ b/RIADigiDoc/UI/Component/My eID/MyEidPinsAndCertificatesView.swift @@ -59,7 +59,7 @@ struct MyEidPinsAndCertificatesView: View { ) } - private var pinBlockedMessage: String { + private var pin1BlockedMessage: String { let pinBlockedText = languageSettings.localized( "PIN blocked", [CodeType.pin1.name] @@ -71,6 +71,19 @@ struct MyEidPinsAndCertificatesView: View { return "\(pinBlockedText) \(languageSettings.localized("PIN blocked unblock message", []))" } + + private var pin2BlockedMessage: String { + let pinBlockedText = languageSettings.localized( + "PIN blocked", + [CodeType.pin2.name] + ) + + if isPukBlocked { + return pinBlockedText + } + + return "\(pinBlockedText) \(languageSettings.localized("PIN blocked unblock message", []))" + } init( isPin1Blocked: Binding, @@ -115,7 +128,7 @@ struct MyEidPinsAndCertificatesView: View { .opacity(opacityForPin1BlockedState) if isPin1Blocked { - Text(verbatim: pinBlockedMessage) + Text(verbatim: pin1BlockedMessage) .font(typography.bodySmall) .foregroundStyle(theme.error) .padding(.vertical, Dimensions.Padding.XSPadding) @@ -149,7 +162,7 @@ struct MyEidPinsAndCertificatesView: View { .opacity(opacityForPin2BlockedState) if isPin2Blocked { - Text(verbatim: pinBlockedMessage) + Text(verbatim: pin2BlockedMessage) .font(typography.bodySmall) .foregroundStyle(theme.error) .padding(.vertical, Dimensions.Padding.XSPadding) @@ -170,9 +183,6 @@ struct MyEidPinsAndCertificatesView: View { } } .padding(.bottom, Dimensions.Padding.SPadding) - .onAppear { - - } VStack { MyEidCertificateCardView( diff --git a/RIADigiDoc/ViewModel/Signing/IdCard/IdCardViewModel.swift b/RIADigiDoc/ViewModel/Signing/IdCard/IdCardViewModel.swift index c7174547..b8c76b00 100644 --- a/RIADigiDoc/ViewModel/Signing/IdCard/IdCardViewModel.swift +++ b/RIADigiDoc/ViewModel/Signing/IdCard/IdCardViewModel.swift @@ -187,7 +187,7 @@ class IdCardViewModel: IdCardViewModelProtocol, Loggable { } } - func getIdCardData() async -> IdCardData? { + func getIdCardData(for codeType: CodeType) async -> IdCardData? { do { let publicData = try await getPublicData() let authCertNotValidDate = try await readAuthenticationCertificateNotValidDate() @@ -195,6 +195,46 @@ class IdCardViewModel: IdCardViewModelProtocol, Loggable { let pinResponse = try await readCodeTryCounterRecord() let isPUKChangeable = try await isPukChangeable() + if (codeType == CodeType.pin1) { + if pinResponse.pin1RetryCount == 0 { + throw IdCardInternalError.remainingPinRetryCount(0) + } + } + if (codeType == CodeType.pin2) { + if pinResponse.pin2RetryCount == 0 { + throw IdCardInternalError.remainingPinRetryCount(0) + } + } + + if !pinResponse.pin2Active { + throw IdCardInternalError.pinLocked + } + + return IdCardData( + publicData: publicData, + authCertNotValidDate: authCertNotValidDate, + signCertNotValidDate: signCertNotValidDate, + pinResponse: pinResponse, + isPUKChangeable: isPUKChangeable + ) + } catch { + IdCardViewModel.logger().error( + "Unable to read ID-card data. \(error)" + ) + + handleError(error, codeType: codeType) + return nil + } + } + + func getIdCardDataMyEid() async -> IdCardData? { + do { + let publicData = try await getPublicData() + let authCertNotValidDate = try await readAuthenticationCertificateNotValidDate() + let signCertNotValidDate = try await readSignatureCertificateNotValidDate() + let pinResponse = try await readCodeTryCounterRecord() + let isPUKChangeable = try await isPukChangeable() + return IdCardData( publicData: publicData, authCertNotValidDate: authCertNotValidDate, diff --git a/RIADigiDoc/ViewModel/Signing/IdCard/IdCardViewModelProtocol.swift b/RIADigiDoc/ViewModel/Signing/IdCard/IdCardViewModelProtocol.swift index 027dd742..25a8394d 100644 --- a/RIADigiDoc/ViewModel/Signing/IdCard/IdCardViewModelProtocol.swift +++ b/RIADigiDoc/ViewModel/Signing/IdCard/IdCardViewModelProtocol.swift @@ -31,7 +31,8 @@ public protocol IdCardViewModelProtocol: Sendable { pin1: String, cryptoContainer: CryptoContainerProtocol? ) async -> CryptoContainerProtocol? - func getIdCardData() async -> IdCardData? + func getIdCardData(for codeType: CodeType) async -> IdCardData? + func getIdCardDataMyEid() async -> IdCardData? func resetErrors() func formatPersonalIdentifier(givenName: String, surname: String, personalCode: String) -> String func isRoleDataEnabled() async -> Bool From 3f564878c649c7bbf63db3f2bfa00939782a8ee5 Mon Sep 17 00:00:00 2001 From: Boriss Melikjan Date: Wed, 18 Feb 2026 11:14:58 +0200 Subject: [PATCH 06/11] MOPPIOS-1659 Fixes. --- RIADigiDoc/ViewModel/Signing/IdCard/IdCardViewModel.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/RIADigiDoc/ViewModel/Signing/IdCard/IdCardViewModel.swift b/RIADigiDoc/ViewModel/Signing/IdCard/IdCardViewModel.swift index b8c76b00..3cd38c5e 100644 --- a/RIADigiDoc/ViewModel/Signing/IdCard/IdCardViewModel.swift +++ b/RIADigiDoc/ViewModel/Signing/IdCard/IdCardViewModel.swift @@ -361,7 +361,6 @@ class IdCardViewModel: IdCardViewModelProtocol, Loggable { } else { errorMessage = "PIN blocked" errorExtraArguments = [pinType.name] - shouldDismissForError = true } case .sessionError: errorMessage = "General error" From 3bc99c56d45ff314f5cf968390237fb9de00fff4 Mon Sep 17 00:00:00 2001 From: Marten Rebane Date: Wed, 18 Feb 2026 11:23:55 +0200 Subject: [PATCH 07/11] Fix My eID back button, PIN error handling --- RIADigiDoc/Domain/NFC/NFCOperationBase.swift | 2 +- RIADigiDoc/Domain/NFC/OperationDecrypt.swift | 7 ++-- .../Domain/NFC/OperationReadCertAndSign.swift | 7 ++-- .../Container/Signing/IdCard/IdCardView.swift | 36 +++++++++++++++-- .../My eID/MyEidPinsAndCertificatesView.swift | 40 +++++++++---------- .../UI/Component/My eID/MyEidView.swift | 5 +-- .../MyEid/MyEidPinChangeViewModel.swift | 11 +++-- .../Signing/IdCard/IdCardViewModel.swift | 33 ++++++++------- .../Signing/SmartId/SmartIdViewModel.swift | 1 + 9 files changed, 88 insertions(+), 54 deletions(-) diff --git a/RIADigiDoc/Domain/NFC/NFCOperationBase.swift b/RIADigiDoc/Domain/NFC/NFCOperationBase.swift index 347da08b..eb4c47bd 100644 --- a/RIADigiDoc/Domain/NFC/NFCOperationBase.swift +++ b/RIADigiDoc/Domain/NFC/NFCOperationBase.swift @@ -82,7 +82,7 @@ public class NFCOperationBase: NSObject, Loggable, @MainActor NFCTagReaderSessio let idCardError = error.getIdCardError() Self.logger().error("NFC: IdCardError detected: \(idCardError)") handleIdCardError(idCardError) - session.invalidate(errorMessage: nfcError ?? "") + session.invalidate(errorMessage: nfcError) } func handleUnknownError( diff --git a/RIADigiDoc/Domain/NFC/OperationDecrypt.swift b/RIADigiDoc/Domain/NFC/OperationDecrypt.swift index cb3ce5dc..20df9521 100644 --- a/RIADigiDoc/Domain/NFC/OperationDecrypt.swift +++ b/RIADigiDoc/Domain/NFC/OperationDecrypt.swift @@ -106,13 +106,13 @@ public class OperationDecrypt: NFCOperationBase { updateAlertMessage(step: 2) let cardCommands = try await connection.getCardCommands(session, tag: tag, CAN: canNumber) updateAlertMessage(step: 3) - + let (retryCount, _) = try await cardCommands.readCodeTryCounterRecord(.pin1) - + if retryCount == 0 { throw IdCardInternalError.remainingPinRetryCount(Int(retryCount)) } - + let cert = try await cardCommands.readAuthenticationCertificate() updateAlertMessage(step: 4) let decryptedContainer = try await CryptoContainer.decrypt( @@ -163,5 +163,4 @@ public class OperationDecrypt: NFCOperationBase { } continuation?.resume(throwing: error) } - } diff --git a/RIADigiDoc/Domain/NFC/OperationReadCertAndSign.swift b/RIADigiDoc/Domain/NFC/OperationReadCertAndSign.swift index a75ec501..2f71c86e 100644 --- a/RIADigiDoc/Domain/NFC/OperationReadCertAndSign.swift +++ b/RIADigiDoc/Domain/NFC/OperationReadCertAndSign.swift @@ -74,6 +74,7 @@ public class OperationReadCertAndSign: NFCOperationBase { // MARK: - NFCTagReaderSessionDelegate + // swiftlint:disable:next cyclomatic_complexity public override func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) { Task { @MainActor in defer { @@ -123,16 +124,16 @@ public class OperationReadCertAndSign: NFCOperationBase { let cardCommands = try await connection.getCardCommands(session, tag: tag, CAN: canNumber) updateAlertMessage(step: 3) - + let (retryCount, pinActive) = try await cardCommands.readCodeTryCounterRecord(.pin2) - + if retryCount == 0 { throw IdCardInternalError.remainingPinRetryCount(Int(retryCount)) } if !pinActive { throw IdCardInternalError.pinLocked } - + let cert = try await cardCommands.readSignatureCertificate() let hashToSign = try await signedContainer.prepareSignature( cert: cert, diff --git a/RIADigiDoc/UI/Component/Container/Signing/IdCard/IdCardView.swift b/RIADigiDoc/UI/Component/Container/Signing/IdCard/IdCardView.swift index 43980c81..b7968440 100644 --- a/RIADigiDoc/UI/Component/Container/Signing/IdCard/IdCardView.swift +++ b/RIADigiDoc/UI/Component/Container/Signing/IdCard/IdCardView.swift @@ -53,6 +53,24 @@ struct IdCardView: View { let onSuccess: (SignedContainerProtocol) -> Void let onSuccessDecrypt: (CryptoContainerProtocol) -> Void + var localizedPinBlockedMessage: String? { + guard let messageKey = viewModel.idCardAlertMessageKey else { return nil } + if messageKey == "PIN blocked" { + let pinBlockedMessage = languageSettings.localized( + messageKey, + viewModel.idCardAlertMessageExtraArguments + ) + + let unblockMessage = languageSettings.localized( + "PIN blocked unblock message" + ) + + return "\(pinBlockedMessage) \(unblockMessage)" + } + + return messageKey + } + var localizedArguments: [String] { var localized: [String] = [] for arg in viewModel.idCardAlertMessageExtraArguments { @@ -94,6 +112,10 @@ struct IdCardView: View { actionType == .signing ? .pin2 : .pin1 } + private var isIdCardAlertError: Bool { + viewModel.idCardAlertMessageKey?.isEmpty == false + } + init( actionType: ActionType, actionMethods: [ActionMethod], @@ -201,7 +223,7 @@ struct IdCardView: View { } case .myeid: // Do nothing - isInProgress = true + break } }, content: { @@ -252,12 +274,15 @@ struct IdCardView: View { } .alert( languageSettings.localized( - viewModel.idCardAlertMessageKey ?? "", + localizedPinBlockedMessage ?? "", localizedArguments ), isPresented: $viewModel.showIdCardAlertMessage ) { Button(languageSettings.localized("OK")) { + Task { + await viewModel.stopDiscoveringReaders() + } viewModel.resetErrors() viewModel.resetAlertErrors() dismiss() @@ -363,7 +388,12 @@ struct IdCardView: View { await MainActor.run { Toast.show(errorMessage) viewModel.resetErrors() - dismiss() + + // Let ID-card alert closure handle dismiss + // Dismiss is run when user has pressed OK button on alert + if !isIdCardAlertError { + dismiss() + } } } diff --git a/RIADigiDoc/UI/Component/My eID/MyEidPinsAndCertificatesView.swift b/RIADigiDoc/UI/Component/My eID/MyEidPinsAndCertificatesView.swift index ecf26264..f1a72b79 100644 --- a/RIADigiDoc/UI/Component/My eID/MyEidPinsAndCertificatesView.swift +++ b/RIADigiDoc/UI/Component/My eID/MyEidPinsAndCertificatesView.swift @@ -71,7 +71,7 @@ struct MyEidPinsAndCertificatesView: View { return "\(pinBlockedText) \(languageSettings.localized("PIN blocked unblock message", []))" } - + private var pin2BlockedMessage: String { let pinBlockedText = languageSettings.localized( "PIN blocked", @@ -106,7 +106,7 @@ struct MyEidPinsAndCertificatesView: View { } var body: some View { - VStack { + VStack(alignment: .leading) { MyEidCertificateCardView( title: languageSettings.localized("Authentication certificate"), subtitle: AttributedString(languageSettings @@ -136,7 +136,7 @@ struct MyEidPinsAndCertificatesView: View { } .padding(.vertical, Dimensions.Padding.SPadding) - VStack { + VStack(alignment: .leading) { MyEidCertificateCardView( title: languageSettings.localized("Signing certificate"), subtitle: AttributedString( @@ -169,22 +169,20 @@ struct MyEidPinsAndCertificatesView: View { } if !isPin2Activated { - VStack(alignment: .leading) { - Text(verbatim: pin2LockedMessage) - .font(typography.bodySmall) - .foregroundStyle(theme.error) - .padding(.vertical, Dimensions.Padding.XSPadding) + Text(verbatim: pin2LockedMessage) + .font(typography.bodySmall) + .foregroundStyle(theme.error) + .padding(.vertical, Dimensions.Padding.XSPadding) - if let pin2LockedInfoUrl = URL(string: pin2LockedUrl) { - additionalInformationLink(url: pin2LockedInfoUrl) - .padding(.vertical, Dimensions.Padding.XSPadding) - } + if let pin2LockedInfoUrl = URL(string: pin2LockedUrl) { + additionalInformationLink(url: pin2LockedInfoUrl) + .padding(.vertical, Dimensions.Padding.XSPadding) } } } .padding(.bottom, Dimensions.Padding.SPadding) - VStack { + VStack(alignment: .leading) { MyEidCertificateCardView( title: pukCodeTitle, subtitle: pukCodeInfo, @@ -197,16 +195,14 @@ struct MyEidPinsAndCertificatesView: View { .accessibilityAddTraits([.isButton]) if isPukBlocked { - VStack(alignment: .leading) { - Text(verbatim: pukBlockedMessage) - .font(typography.bodySmall) - .foregroundStyle(theme.error) - .padding(.vertical, Dimensions.Padding.XSPadding) + Text(verbatim: pukBlockedMessage) + .font(typography.bodySmall) + .foregroundStyle(theme.error) + .padding(.vertical, Dimensions.Padding.XSPadding) - if let pukBlockedInfoUrl = URL(string: pukBlockedUrl) { - additionalInformationLink(url: pukBlockedInfoUrl) - .padding(.vertical, Dimensions.Padding.XSPadding) - } + if let pukBlockedInfoUrl = URL(string: pukBlockedUrl) { + additionalInformationLink(url: pukBlockedInfoUrl) + .padding(.vertical, Dimensions.Padding.XSPadding) } } } diff --git a/RIADigiDoc/UI/Component/My eID/MyEidView.swift b/RIADigiDoc/UI/Component/My eID/MyEidView.swift index 64db6355..c1eb2140 100644 --- a/RIADigiDoc/UI/Component/My eID/MyEidView.swift +++ b/RIADigiDoc/UI/Component/My eID/MyEidView.swift @@ -101,10 +101,9 @@ struct MyEidView: View { onLeftClick: { Task { await viewModel.stopDiscoveringReaders() - await MainActor.run { - dismiss() - } } + + dismiss() }, content: { ScrollView { diff --git a/RIADigiDoc/ViewModel/MyEid/MyEidPinChangeViewModel.swift b/RIADigiDoc/ViewModel/MyEid/MyEidPinChangeViewModel.swift index 77d234a7..e03b72a0 100644 --- a/RIADigiDoc/ViewModel/MyEid/MyEidPinChangeViewModel.swift +++ b/RIADigiDoc/ViewModel/MyEid/MyEidPinChangeViewModel.swift @@ -278,11 +278,14 @@ final class MyEidPinChangeViewModel: MyEidPinChangeViewModelProtocol, Loggable { if let idCardInternalError = error as? IdCardInternalError { let idCardError = idCardInternalError.getIdCardError() - MyEidPinChangeViewModel.logger().error("NFC: IdCardError: \(idCardError)") + MyEidPinChangeViewModel.logger().error("IdCardInternalError: \(idCardError)") + handleIdCardError(idCardError, pinType: codeType) + } else if let idCardError = error as? IdCardError { + MyEidPinChangeViewModel.logger().error("IdCardError: \(idCardError)") handleIdCardError(idCardError, pinType: codeType) } else { - MyEidPinChangeViewModel.logger().error("NFC: Unexpected error type: \(type(of: error))") - MyEidPinChangeViewModel.logger().error("NFC: Error details: \(error)") + MyEidPinChangeViewModel.logger().error("Unexpected error type: \(type(of: error))") + MyEidPinChangeViewModel.logger().error("Error details: \(error)") errorMessage = "General error" } @@ -318,7 +321,7 @@ final class MyEidPinChangeViewModel: MyEidPinChangeViewModelProtocol, Loggable { sharedMyEidSession.setIsPinBlocked(codeType, isBlocked: true) } case .sessionError: - errorMessage = "NFC session error" + errorMessage = "General error" errorMessageExtraArguments = [] default: resetInputError() diff --git a/RIADigiDoc/ViewModel/Signing/IdCard/IdCardViewModel.swift b/RIADigiDoc/ViewModel/Signing/IdCard/IdCardViewModel.swift index 3cd38c5e..dbb7dd52 100644 --- a/RIADigiDoc/ViewModel/Signing/IdCard/IdCardViewModel.swift +++ b/RIADigiDoc/ViewModel/Signing/IdCard/IdCardViewModel.swift @@ -92,13 +92,13 @@ class IdCardViewModel: IdCardViewModelProtocol, Loggable { let pinSecureData = SecureData(Array(pin1.utf8)) let cardCommands = try await idCardRepository.getCardHandler() - + let (retryCount, _) = try await idCardRepository.readCodeTryCounterRecord(for: .pin1) - + if retryCount == 0 { throw IdCardInternalError.remainingPinRetryCount(Int(retryCount)) } - + let authCertData = try await idCardRepository.readAuthenticationCertificate() let container = try await CryptoContainer.decrypt( containerFile: containerFile, @@ -133,16 +133,16 @@ class IdCardViewModel: IdCardViewModelProtocol, Loggable { do { let containerFile = await signedContainer.getRawContainerFile() ?? URL(fileURLWithPath: "") let pinSecureData = SecureData(Array(pin2.utf8)) - + let (retryCount, pinActive) = try await idCardRepository.readCodeTryCounterRecord(for: .pin2) - + if retryCount == 0 { throw IdCardInternalError.remainingPinRetryCount(Int(retryCount)) } if !pinActive { throw IdCardInternalError.pinLocked } - + let signatureCertificate = try await idCardRepository.readSignatureCertificate() IdCardViewModel.logger().info("ID-CARD: Getting language") @@ -195,21 +195,21 @@ class IdCardViewModel: IdCardViewModelProtocol, Loggable { let pinResponse = try await readCodeTryCounterRecord() let isPUKChangeable = try await isPukChangeable() - if (codeType == CodeType.pin1) { + if codeType == CodeType.pin1 { if pinResponse.pin1RetryCount == 0 { throw IdCardInternalError.remainingPinRetryCount(0) } } - if (codeType == CodeType.pin2) { + if codeType == CodeType.pin2 { if pinResponse.pin2RetryCount == 0 { throw IdCardInternalError.remainingPinRetryCount(0) } } - + if !pinResponse.pin2Active { throw IdCardInternalError.pinLocked } - + return IdCardData( publicData: publicData, authCertNotValidDate: authCertNotValidDate, @@ -226,7 +226,7 @@ class IdCardViewModel: IdCardViewModelProtocol, Loggable { return nil } } - + func getIdCardDataMyEid() async -> IdCardData? { do { let publicData = try await getPublicData() @@ -234,7 +234,7 @@ class IdCardViewModel: IdCardViewModelProtocol, Loggable { let signCertNotValidDate = try await readSignatureCertificateNotValidDate() let pinResponse = try await readCodeTryCounterRecord() let isPUKChangeable = try await isPukChangeable() - + return IdCardData( publicData: publicData, authCertNotValidDate: authCertNotValidDate, @@ -359,8 +359,9 @@ class IdCardViewModel: IdCardViewModelProtocol, Loggable { errorMessage = "PIN verification error one" errorExtraArguments = [pinType.name] } else { - errorMessage = "PIN blocked" - errorExtraArguments = [pinType.name] + showIdCardAlertMessage = true + idCardAlertMessageKey = "PIN blocked" + idCardAlertMessageExtraArguments = [pinType.name] } case .sessionError: errorMessage = "General error" @@ -420,6 +421,10 @@ class IdCardViewModel: IdCardViewModelProtocol, Loggable { case let idCardError as IdCardError: handleIdCardError(idCardError, pinType: codeType) + case let idCardInternalError as IdCardInternalError: + let idCardError = idCardInternalError.getIdCardError() + handleIdCardError(idCardError, pinType: codeType) + case let digidocError as DigiDocError: handleSignatureAddingError(digidocError) diff --git a/RIADigiDoc/ViewModel/Signing/SmartId/SmartIdViewModel.swift b/RIADigiDoc/ViewModel/Signing/SmartId/SmartIdViewModel.swift index 84299444..a6f64e09 100644 --- a/RIADigiDoc/ViewModel/Signing/SmartId/SmartIdViewModel.swift +++ b/RIADigiDoc/ViewModel/Signing/SmartId/SmartIdViewModel.swift @@ -118,6 +118,7 @@ class SmartIdViewModel: SmartIdViewModelProtocol, Loggable { smartIdAlertMessageUrl = nil } + // swiftlint:disable:next cyclomatic_complexity func sign( country: SmartIdCountry, personalCode: String, From bc4f4fa7863ad644548a4eecf9f8624b0881ccf3 Mon Sep 17 00:00:00 2001 From: Boriss Melikjan Date: Wed, 18 Feb 2026 14:47:56 +0200 Subject: [PATCH 08/11] MOPPIOS-1659 Fix for showing Change PUK button in modal. --- .../Signing/Modal/ConfirmModalView.swift | 2 ++ RIADigiDoc/UI/Component/My eID/MyEidView.swift | 10 ++++++++-- .../Shared/Modal/ModalContainer.swift | 18 ++++++++++-------- .../UI/Component/Shared/Modal/TextModal.swift | 2 ++ 4 files changed, 22 insertions(+), 10 deletions(-) diff --git a/RIADigiDoc/UI/Component/Container/Signing/Modal/ConfirmModalView.swift b/RIADigiDoc/UI/Component/Container/Signing/Modal/ConfirmModalView.swift index 9bcfaeeb..455e4f0b 100644 --- a/RIADigiDoc/UI/Component/Container/Signing/Modal/ConfirmModalView.swift +++ b/RIADigiDoc/UI/Component/Container/Signing/Modal/ConfirmModalView.swift @@ -22,6 +22,7 @@ import SwiftUI struct ConfirmModalView: View { var title: String var message: String + var isConfirmButtonVisible: Bool = true var confirmButtonTitle: String = "Remove" var cancelButtonTitle: String = "Cancel" var confirmButtonAccessibility: String? @@ -39,6 +40,7 @@ struct ConfirmModalView: View { TextModal( title: title, message: message, + isConfirmButtonVisible: isConfirmButtonVisible, confirmButtonTitle: confirmButtonTitle, cancelButtonTitle: cancelButtonTitle, confirmButtonAccessibility: confirmButtonAccessibility, diff --git a/RIADigiDoc/UI/Component/My eID/MyEidView.swift b/RIADigiDoc/UI/Component/My eID/MyEidView.swift index c1eb2140..7d1d4381 100644 --- a/RIADigiDoc/UI/Component/My eID/MyEidView.swift +++ b/RIADigiDoc/UI/Component/My eID/MyEidView.swift @@ -235,12 +235,18 @@ struct MyEidView: View { onConfirm: @escaping () -> Void, onCancel: @escaping () -> Void ) -> some View { - + let config = configuration(for: pinVariant) - + let isConfirmButtonVisible = + if config.codeType == .puk { + idCardData.isPUKChangeable + } else { + true + } ConfirmModalView( title: languageSettings.localized("PIN guideline title", [config.codeType.name]), message: config.guidelines, + isConfirmButtonVisible: isConfirmButtonVisible, confirmButtonTitle: config.confirmTitle, cancelButtonTitle: languageSettings.localized("Close"), onConfirm: onConfirm, diff --git a/RIADigiDoc/UI/Component/Shared/Modal/ModalContainer.swift b/RIADigiDoc/UI/Component/Shared/Modal/ModalContainer.swift index b23e033c..25ee1f2c 100644 --- a/RIADigiDoc/UI/Component/Shared/Modal/ModalContainer.swift +++ b/RIADigiDoc/UI/Component/Shared/Modal/ModalContainer.swift @@ -28,6 +28,7 @@ struct ModalContainer: View { var icon: String? var title: String + var isConfirmButtonVisible: Bool = true var confirmButtonTitle: String = "OK" var cancelButtonTitle: String = "Cancel" var confirmButtonAccessibility: String? @@ -68,14 +69,15 @@ struct ModalContainer: View { cancelButtonAccessibility ?? languageSettings.localized(cancelButtonTitle).lowercased() ) - - Button(languageSettings.localized(confirmButtonTitle)) { onConfirm() } - .font(typography.labelLarge) - .foregroundStyle(theme.primary) - .accessibilityLabel( - confirmButtonAccessibility ?? - languageSettings.localized(confirmButtonTitle).lowercased() - ) + if (isConfirmButtonVisible) { + Button(languageSettings.localized(confirmButtonTitle)) { onConfirm() } + .font(typography.labelLarge) + .foregroundStyle(theme.primary) + .accessibilityLabel( + confirmButtonAccessibility ?? + languageSettings.localized(confirmButtonTitle).lowercased() + ) + } } .frame(maxWidth: .infinity, alignment: .trailing) .padding(.vertical, Dimensions.Padding.MSPadding) diff --git a/RIADigiDoc/UI/Component/Shared/Modal/TextModal.swift b/RIADigiDoc/UI/Component/Shared/Modal/TextModal.swift index 3a4eb687..cc877b26 100644 --- a/RIADigiDoc/UI/Component/Shared/Modal/TextModal.swift +++ b/RIADigiDoc/UI/Component/Shared/Modal/TextModal.swift @@ -26,6 +26,7 @@ struct TextModal: View { var icon: String? var title: String var message: String + var isConfirmButtonVisible: Bool = true var confirmButtonTitle: String = "OK" var cancelButtonTitle: String = "Cancel" var confirmButtonAccessibility: String? @@ -37,6 +38,7 @@ struct TextModal: View { ModalContainer( icon: icon, title: title, + isConfirmButtonVisible: isConfirmButtonVisible, confirmButtonTitle: confirmButtonTitle, cancelButtonTitle: cancelButtonTitle, confirmButtonAccessibility: confirmButtonAccessibility, From ee21608f8b293722d9336173acf3349050338622 Mon Sep 17 00:00:00 2001 From: Boriss Melikjan Date: Wed, 18 Feb 2026 14:56:30 +0200 Subject: [PATCH 09/11] MOPPIOS-1659 Hide PUK change dialog for Thales cards. --- .../UI/Component/My eID/MyEidPinsAndCertificatesView.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/RIADigiDoc/UI/Component/My eID/MyEidPinsAndCertificatesView.swift b/RIADigiDoc/UI/Component/My eID/MyEidPinsAndCertificatesView.swift index f1a72b79..df4151e8 100644 --- a/RIADigiDoc/UI/Component/My eID/MyEidPinsAndCertificatesView.swift +++ b/RIADigiDoc/UI/Component/My eID/MyEidPinsAndCertificatesView.swift @@ -190,7 +190,9 @@ struct MyEidPinsAndCertificatesView: View { ) .opacity(opacityForPukBlockedState) .onTapGesture { - pinChangeVariant = .pukChange + if (isPUKChangeable) { + pinChangeVariant = .pukChange + } } .accessibilityAddTraits([.isButton]) From e04f40dbbaa1bd890d68926625b3da94fcf020a9 Mon Sep 17 00:00:00 2001 From: Boriss Melikjan Date: Wed, 18 Feb 2026 16:28:27 +0200 Subject: [PATCH 10/11] MOPPIOS-1659 Fix for libcdoc logs message. --- .../Sources/CryptoObjC/include/Encrypt.mm | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/Modules/CryptoLib/Sources/CryptoObjC/include/Encrypt.mm b/Modules/CryptoLib/Sources/CryptoObjC/include/Encrypt.mm index c4db1c93..b4627da5 100644 --- a/Modules/CryptoLib/Sources/CryptoObjC/include/Encrypt.mm +++ b/Modules/CryptoLib/Sources/CryptoObjC/include/Encrypt.mm @@ -78,6 +78,19 @@ + (void)setProxy:(nonnull NSString *)host port:(NSInteger)port username:(nonnull return @"UNKNOWN"; } +static inline NSString *BasenameFromPath(NSString *path) { + if (path.length == 0) return @""; + NSString *last = path.lastPathComponent; + if (last.length > 0) return last; + + NSCharacterSet *seps = [NSCharacterSet characterSetWithCharactersInString:@"/\\"]; + NSArray *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, @@ -85,7 +98,8 @@ void LogMessage(libcdoc::ILogger::LogLevel level, 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); From 8f13245b6db68c89506d47b572bdb9cc31710675 Mon Sep 17 00:00:00 2001 From: Boriss Melikjan Date: Wed, 18 Feb 2026 16:36:06 +0200 Subject: [PATCH 11/11] MOPPIOS-1659 Code review suggestions. --- Modules/CryptoLib/Sources/CryptoObjC/include/Encrypt.mm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CryptoLib/Sources/CryptoObjC/include/Encrypt.mm b/Modules/CryptoLib/Sources/CryptoObjC/include/Encrypt.mm index b4627da5..e3e12953 100644 --- a/Modules/CryptoLib/Sources/CryptoObjC/include/Encrypt.mm +++ b/Modules/CryptoLib/Sources/CryptoObjC/include/Encrypt.mm @@ -79,7 +79,7 @@ + (void)setProxy:(nonnull NSString *)host port:(NSInteger)port username:(nonnull } static inline NSString *BasenameFromPath(NSString *path) { - if (path.length == 0) return @""; + if (path == nil || path.length == 0) return @""; NSString *last = path.lastPathComponent; if (last.length > 0) return last;