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 e7d7f4e5..8d09105b 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") @@ -273,13 +283,92 @@ class CardReaderNFC: @unchecked CardReader, Loggable { return (.init(), UInt16(tlvRes.value[0], tlvRes.value[1])) } let ivValue = try AES.CBC(key: ksEnc).encrypt(SSC) - let responseData = try (try AES.CBC(key: ksEnc, ivVal: ivValue).decrypt(tlvEnc.value[1...])).removePadding() + let responseData = try ( + try AES.CBC(key: ksEnc, ivVal: ivValue) + .decrypt(tlvEnc.tag == 0x85 ? tlvEnc.value : tlvEnc.value[1...])) + .removePadding() CardReaderNFC.logger().info("Plain <: \(responseData.toHex) \(tlvRes.value.toHex)") return (Bytes(responseData), UInt16(tlvRes.value[0], tlvRes.value[1])) } // MARK: - Utils + static private func pseudoRandomNumberMappingAES( + sVal: any AES.DataType, + tVal: any AES.DataType, + domain: Domain + ) throws -> BInt { + let lVal = sVal.count * 8 + let kVal = tVal.count * 8 + + let c0Val: Bytes + let c1Val: Bytes + switch lVal { + case 128: + c0Val = Bytes(hex: "a668892a7c41e3ca739f40b057d85904")! + c1Val = Bytes(hex: "a4e136ac725f738b01c1f60217c188ad")! + case 192, 256: + c0Val = Bytes(hex: "d463d65234124ef7897054986dca0a174e28df758cbaa03f240616414d5a1676")! + c1Val = Bytes(hex: "54bd7255f0aaf831bec3423fcf39d69b6cbf066677d0faae5aadd99df8e53517")! + default: + throw IdCardInternalError.authenticationFailed + } + + let cipher = AES.CBC(key: tVal) + var key = try cipher.encrypt(sVal) + + var xVal = Bytes() + var nVal = 0 + while nVal * lVal < domain.p.bitWidth + 64 { + let cipher = AES.CBC(key: key.prefix(kVal / 8)) + key = try cipher.encrypt(c0Val) + xVal += try cipher.encrypt(c1Val) + nVal += 1 + } + + return BInt(magnitude: xVal).mod(domain.p) + } + + /** + * https://www.icao.int/Security/FAL/TRIP/Documents/TR%20-%20Supplemental%20Access%20Control%20V1.1.pdf + * A.2.1. Implementation for affine coordinates + */ + static private func pointEncodeIM(tVal: BInt, domain: Domain) -> Point { + let pVal = domain.p + let aVal = domain.a + let bVal = domain.b + + // 1. α = -t^2 mod p + let alpha = (-(tVal ** 2)).mod(pVal) + + // 2. X2 = -ba^-1 (1 + (α + α^2)^-1) mod p + // Hint = -b(1 + α + α^2)(a(α + α^2))^(p-2) mod p + let alphaPlusAlphaSqrt = alpha + alpha ** 2 + let x2Val = ((-bVal * (1 + alphaPlusAlphaSqrt)) * (aVal * alphaPlusAlphaSqrt).expMod(pVal - 2, pVal)).mod(pVal) + + // 3. X3 = α * X2 mod p + let x3Val = (alpha * x2Val).mod(pVal) + + // 4. h2 = (X2)^3 + a * X2 + b mod p + let h2Val = (x2Val ** 3 + aVal * x2Val + bVal).mod(pVal) + + // 5. h3 = (X3)^3 + a * X3 + b mod p + // Unused: let h3 = (X3 ** 3 + a * X3 + b).mod(p) + + // 6. U = t^3 * h2 mod p + let UVal = (tVal ** 3 * h2Val).mod(pVal) + + // 7. A = (h2)^(p - 1 - (p + 1) / 4) mod p + // Hint: modular exponentiation with exponent p-1-(p+1)/4. + let AVal = h2Val.expMod(pVal - BInt.ONE - (pVal + BInt.ONE) / BInt.FOUR, pVal) + + // 8. A^2 * h2 mod p = 1 -> (x, y) = (X2, A h2 mod p) + // 9. (x, y) = (X3, A U mod p) + return (AVal ** 2 * h2Val).mod(pVal) == BInt.ONE ? + Point(x2Val, (AVal * h2Val).mod(pVal)) : + Point(x3Val, (AVal * UVal).mod(pVal)) + } + static private func decryptNonce(CAN: String, encryptedNonce: T) throws -> Bytes { let decryptionKey = KDF(key: Bytes(CAN.utf8), counter: 3) let cipher = AES.CBC(key: decryptionKey) @@ -298,7 +387,19 @@ class CardReaderNFC: @unchecked CardReader, Loggable { initializedCount = Int(CC_SHA256_DIGEST_LENGTH) } } + + static private func random(count: Int) throws -> Data { + var data = Data(count: count) + let result = data.withUnsafeMutableBytes { buffer in + SecRandomCopyBytes(kSecRandomDefault, count, buffer.baseAddress!) + } + if result != errSecSuccess { + throw IdCardInternalError.authenticationFailed + } + return data + } } +// swiftlint:enable force_unwrapping // MARK: - Extensions 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 2341b2af..43fd2d63 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 100583f3..ea492924 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 2e01948e..916fa51b 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 cc8b2ce8..7ff385e5 100644 --- a/RIADigiDoc.xcodeproj/project.pbxproj +++ b/RIADigiDoc.xcodeproj/project.pbxproj @@ -150,7 +150,7 @@ Domain/Model/Error/ReadCertAndSignError.swift, Domain/Model/FileItem.swift, Domain/Model/IdCard/IdCardData.swift, - Domain/Model/IdCard/RetryCount.swift, + Domain/Model/IdCard/PinResponse.swift, Domain/Model/KeychainKey.swift, "Domain/Model/My eID/MyEidDocumentStatus.swift", "Domain/Model/My eID/MyEidPinCodeAction.swift", 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 1703d1b0..5bda59a8 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 d376244e..d2b580eb 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 f24af8dc..450e4875 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 5836dfe6..7bc54b45 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 f43579bc..9a8050ee 100644 --- a/RIADigiDoc/Supporting files/Localizable.xcstrings +++ b/RIADigiDoc/Supporting files/Localizable.xcstrings @@ -5448,6 +5448,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/Container/Signing/NFC/NFCView.swift b/RIADigiDoc/UI/Component/Container/Signing/NFC/NFCView.swift index e8871b0c..6fc20bd1 100644 --- a/RIADigiDoc/UI/Component/Container/Signing/NFC/NFCView.swift +++ b/RIADigiDoc/UI/Component/Container/Signing/NFC/NFCView.swift @@ -165,10 +165,13 @@ struct NFCView: View { publicData: CardInfo(), 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/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 a0b5f1ba..e295a5b0 100644 --- a/RIADigiDoc/UI/Component/My eID/MyEidView.swift +++ b/RIADigiDoc/UI/Component/My eID/MyEidView.swift @@ -82,9 +82,13 @@ struct MyEidView: View { _viewModel = State(wrappedValue: Container.shared.myEidViewModel()) self.idCardData = idCardData - 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 { @@ -126,6 +130,7 @@ struct MyEidView: View { isPin1Blocked: $isPin1Blocked, isPin2Blocked: $isPin2Blocked, isPukBlocked: $isPukBlocked, + isPin2Activated: $isPin2Activated, pinChangeVariant: $pinChangeVariant, authCertValidTo: idCardData.authCertNotValidDate ?? "", signCertValidTo: idCardData.signCertNotValidDate ?? "", @@ -209,6 +214,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) @@ -291,10 +297,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 be41624a..e1797667 100644 --- a/RIADigiDoc/ViewModel/MyEid/Shared/SharedMyEidSession.swift +++ b/RIADigiDoc/ViewModel/MyEid/Shared/SharedMyEidSession.swift @@ -30,6 +30,10 @@ final class SharedMyEidSession: SharedMyEidSessionProtocol { private var isPin2Blocked = false private var isPukBlocked = false + private var isPin1Locked = false + private var isPin2Locked = false + private var isPukLocked = false + private let idCardRepository: IdCardRepositoryProtocol private var task: Task? @@ -38,6 +42,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 cac7cec5..a5101b3d 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 2c915b6b..4e2409ac 100644 --- a/RIADigiDoc/ViewModel/Signing/IdCard/IdCardViewModel.swift +++ b/RIADigiDoc/ViewModel/Signing/IdCard/IdCardViewModel.swift @@ -111,14 +111,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 { @@ -172,18 +172,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, ) }