From 8239c3d59a5e0eab992dfe6b3a7a3012ec6a237d Mon Sep 17 00:00:00 2001 From: Raul Metsma Date: Thu, 14 Sep 2023 16:14:44 +0300 Subject: [PATCH 1/6] NFC Support IB-7733 Signed-off-by: Raul Metsma --- MoppApp/MoppApp.xcodeproj/project.pbxproj | 28 +- MoppApp/MoppApp/Info.plist | 6 + MoppApp/MoppApp/KeychainUtil.swift | 48 +- MoppApp/MoppApp/LocalizationKeys.swift | 3 + MoppApp/MoppApp/MoppApp.entitlements | 4 + MoppApp/MoppApp/NFCEditViewController.swift | 222 +++++++ MoppApp/MoppApp/NFCSignature.swift | 555 ++++++++++++++++++ MoppApp/MoppApp/SigningActions.swift | 9 + MoppApp/MoppApp/TokenFlow.storyboard | 225 ++++++- .../TokenFlowSelectionViewController.swift | 30 +- MoppApp/MoppApp/en.lproj/Localizable.strings | 3 + MoppApp/MoppApp/et.lproj/Localizable.strings | 3 + MoppApp/MoppApp/ru.lproj/Localizable.strings | 3 + .../SkSigningLib/Models/SigningType.swift | 1 + 14 files changed, 1103 insertions(+), 37 deletions(-) create mode 100644 MoppApp/MoppApp/NFCEditViewController.swift create mode 100644 MoppApp/MoppApp/NFCSignature.swift diff --git a/MoppApp/MoppApp.xcodeproj/project.pbxproj b/MoppApp/MoppApp.xcodeproj/project.pbxproj index 71a8a45a2..26ed1b0e0 100644 --- a/MoppApp/MoppApp.xcodeproj/project.pbxproj +++ b/MoppApp/MoppApp.xcodeproj/project.pbxproj @@ -30,6 +30,9 @@ 4E59080024B0F914001B23A6 /* SmartIDEditViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E5907FF24B0F914001B23A6 /* SmartIDEditViewController.swift */; }; 4E59080224B258C7001B23A6 /* SmartIDChallengeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E59080124B258C6001B23A6 /* SmartIDChallengeViewController.swift */; }; 4E59080424B2E295001B23A6 /* SmartIDSignature.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E59080324B2E295001B23A6 /* SmartIDSignature.swift */; }; + 4E6E1D9B2AAB493A008B3E74 /* NFCEditViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E6E1D9A2AAB493A008B3E74 /* NFCEditViewController.swift */; }; + 4EE56D232AB0561C002648EE /* NFCSignature.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE56D222AB0561C002648EE /* NFCSignature.swift */; }; + 4EE56D272AB058A6002648EE /* SwiftECC in Frameworks */ = {isa = PBXBuildFile; productRef = 4EE56D262AB058A6002648EE /* SwiftECC */; }; 540786E91E1A76640016ABA7 /* UITextView+Additions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 540786E81E1A76640016ABA7 /* UITextView+Additions.swift */; }; 547BDF251E8BEFA30093931F /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = E4250D0D1E0AA8E500530370 /* Localizable.strings */; }; 54825EF51E1CFE9600253FF0 /* Date+Additions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54825EF41E1CFE9600253FF0 /* Date+Additions.swift */; }; @@ -370,6 +373,8 @@ 4E5907FF24B0F914001B23A6 /* SmartIDEditViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SmartIDEditViewController.swift; sourceTree = ""; }; 4E59080124B258C6001B23A6 /* SmartIDChallengeViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SmartIDChallengeViewController.swift; sourceTree = ""; }; 4E59080324B2E295001B23A6 /* SmartIDSignature.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SmartIDSignature.swift; sourceTree = ""; }; + 4E6E1D9A2AAB493A008B3E74 /* NFCEditViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NFCEditViewController.swift; sourceTree = ""; }; + 4EE56D222AB0561C002648EE /* NFCSignature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NFCSignature.swift; sourceTree = ""; }; 540786E81E1A76640016ABA7 /* UITextView+Additions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UITextView+Additions.swift"; sourceTree = ""; }; 54825EF41E1CFE9600253FF0 /* Date+Additions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Date+Additions.swift"; sourceTree = ""; }; 54825EF71E1D270F00253FF0 /* String+Additions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Additions.swift"; sourceTree = ""; }; @@ -644,6 +649,7 @@ 393B66E820DB9C19001DC89B /* CryptoLib.framework in Frameworks */, DF135630290201A100F61823 /* ExternalAccessory.framework in Frameworks */, DF1503AB2AC5D1E5007222B2 /* ZIPFoundation in Frameworks */, + 4EE56D272AB058A6002648EE /* SwiftECC in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -677,6 +683,7 @@ C54EA6E72045A9E30039AC78 /* IdCardViewController.swift */, C54BB10E1FBD93A100A274F6 /* MobileIDChallengeViewController.swift */, C537AE532004C29E009037E5 /* MobileIDEditViewController.swift */, + 4E6E1D9A2AAB493A008B3E74 /* NFCEditViewController.swift */, 4E59080124B258C6001B23A6 /* SmartIDChallengeViewController.swift */, 4E5907FF24B0F914001B23A6 /* SmartIDEditViewController.swift */, C50DCD1A1FD1576B00D48E16 /* SigningTableViewHeaderView.swift */, @@ -734,8 +741,7 @@ children = ( C50DCCD01FC573FB00D48E16 /* Roboto */, ); - name = Fonts; - path = "New Group"; + path = Fonts; sourceTree = ""; }; C50DCCD01FC573FB00D48E16 /* Roboto */ = { @@ -1202,6 +1208,7 @@ DFDF02C3241ED0CA006CF443 /* MobileIDSignature.swift */, 4E59080324B2E295001B23A6 /* SmartIDSignature.swift */, DFBDF20127DF7ED700A5CF3C /* IDCardSignature.swift */, + 4EE56D222AB0561C002648EE /* NFCSignature.swift */, DFDF02BD241EB5D3006CF443 /* SessionCertificate.swift */, E4C53F4D1E30D13100F209BE /* Session.swift */, DFDF02BF241EC72C006CF443 /* SessionStatus.swift */, @@ -1296,6 +1303,7 @@ DF1503A52AC5D1B9007222B2 /* FirebaseCrashlytics */, DF1503A72AC5D1DB007222B2 /* SwiftyRSA */, DF1503AA2AC5D1E5007222B2 /* ZIPFoundation */, + 4EE56D262AB058A6002648EE /* SwiftECC */, ); productName = MoppApp; productReference = E4250CC31E0968D200530370 /* MoppApp.app */; @@ -1348,6 +1356,7 @@ DF15037C2AC5CD32007222B2 /* XCRemoteSwiftPackageReference "SwiftyRSA" */, DF15037F2AC5CD8D007222B2 /* XCRemoteSwiftPackageReference "ZIPFoundation" */, DF1503822AC5CDBC007222B2 /* XCRemoteSwiftPackageReference "ASN1Decoder" */, + 4EE56D252AB058A6002648EE /* XCRemoteSwiftPackageReference "SwiftECC" */, ); productRefGroup = E4250CC41E0968D200530370 /* Products */; projectDirPath = ""; @@ -1607,6 +1616,7 @@ C50DCD461FDECE2B00D48E16 /* SignatureDetailsViewController.swift in Sources */, DF5BE6D429C8BD5600331609 /* WarningDetail.swift in Sources */, DF32A6812909F0FA00AE5F82 /* FileLogUtil.swift in Sources */, + 4EE56D232AB0561C002648EE /* NFCSignature.swift in Sources */, C50DCCFF1FC6E95A00D48E16 /* ContainerViewController.swift in Sources */, C50DCD481FDED76400D48E16 /* SignatureDetailsCell.swift in Sources */, DF0C2B3F29150EAB007E1745 /* ScaledLabel.swift in Sources */, @@ -1680,6 +1690,7 @@ C506EC811FB9F4AF00E07226 /* UIImage+Additions.swift in Sources */, C55A6E42208766BA000F3386 /* MyeIDChangeCodesLoadingViewController.swift in Sources */, DFDC0ABA29FAD8F2002D1D1D /* ViewUtil.swift in Sources */, + 4E6E1D9B2AAB493A008B3E74 /* NFCEditViewController.swift in Sources */, DFD8BEF5291C432400FE8F07 /* ScaledButton.swift in Sources */, C5927FC720751402003B7F41 /* MyeIDPinPukCell.swift in Sources */, DFF3C3AF233231190079458A /* SettingsConfiguration.swift in Sources */, @@ -2125,6 +2136,14 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ + 4EE56D252AB058A6002648EE /* XCRemoteSwiftPackageReference "SwiftECC" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/leif-ibsen/SwiftECC"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 4.0.0; + }; + }; DF1503792AC5CCE7007222B2 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/firebase/firebase-ios-sdk.git"; @@ -2160,6 +2179,11 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + 4EE56D262AB058A6002648EE /* SwiftECC */ = { + isa = XCSwiftPackageProductDependency; + package = 4EE56D252AB058A6002648EE /* XCRemoteSwiftPackageReference "SwiftECC" */; + productName = SwiftECC; + }; DF1503A32AC5D163007222B2 /* ASN1Decoder */ = { isa = XCSwiftPackageProductDependency; package = DF1503822AC5CDBC007222B2 /* XCRemoteSwiftPackageReference "ASN1Decoder" */; diff --git a/MoppApp/MoppApp/Info.plist b/MoppApp/MoppApp/Info.plist index 6521e28f8..1e8a35295 100644 --- a/MoppApp/MoppApp/Info.plist +++ b/MoppApp/MoppApp/Info.plist @@ -219,6 +219,8 @@ LSSupportsOpeningDocumentsInPlace + NFCReaderUsageDescription + This app uses NFC to scan ID-cards NSAppTransportSecurity NSExceptionDomains @@ -1020,5 +1022,9 @@ + com.apple.developer.nfc.readersession.iso7816.select-identifiers + + A000000077010800070000FE000001 + diff --git a/MoppApp/MoppApp/KeychainUtil.swift b/MoppApp/MoppApp/KeychainUtil.swift index b8c86186c..2e9990799 100644 --- a/MoppApp/MoppApp/KeychainUtil.swift +++ b/MoppApp/MoppApp/KeychainUtil.swift @@ -28,52 +28,52 @@ class KeychainUtil { static func save(key: String, info: String) -> Bool { guard let bundleIdentifier = Bundle.main.bundleIdentifier else { return false } if let data = info.data(using: .utf8) { - let query: [String: Any] = [ - kSecClass as String: kSecClassGenericPassword, - kSecAttrService as String: bundleIdentifier, - kSecAttrAccount as String: "\(bundleIdentifier).\(key)", - kSecValueData as String: data, - kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly + let query: [CFString: Any] = [ + kSecClass: kSecClassGenericPassword, + kSecAttrService: bundleIdentifier, + kSecAttrAccount: "\(bundleIdentifier).\(key)", + kSecValueData: data, + kSecAttrAccessible: kSecAttrAccessibleWhenUnlockedThisDeviceOnly ] SecItemDelete(query as CFDictionary) let status = SecItemAdd(query as CFDictionary, nil) - + return status == errSecSuccess } return false } - + static func retrieve(key: String) -> String? { guard let bundleIdentifier = Bundle.main.bundleIdentifier else { return nil } - let query: [String: Any] = [ - kSecClass as String: kSecClassGenericPassword, - kSecAttrService as String: bundleIdentifier, - kSecAttrAccount as String: "\(bundleIdentifier).\(key)", - kSecReturnData as String: true, - kSecMatchLimit as String: kSecMatchLimitOne, - kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly + let query: [CFString: Any] = [ + kSecClass: kSecClassGenericPassword, + kSecAttrService: bundleIdentifier, + kSecAttrAccount: "\(bundleIdentifier).\(key)", + kSecReturnData: true, + kSecMatchLimit: kSecMatchLimitOne, + kSecAttrAccessible: kSecAttrAccessibleWhenUnlockedThisDeviceOnly ] - + var infoData: AnyObject? let status = SecItemCopyMatching(query as CFDictionary, &infoData) - + if status == errSecSuccess, let data = infoData as? Data { return String(data: data, encoding: .utf8) } else { return nil } } - + static func remove(key: String) { guard let bundleIdentifier = Bundle.main.bundleIdentifier else { return } - let query: [String: Any] = [ - kSecClass as String: kSecClassGenericPassword, - kSecAttrService as String: bundleIdentifier, - kSecAttrAccount as String: "\(bundleIdentifier).\(key)" + let query: [CFString: Any] = [ + kSecClass: kSecClassGenericPassword, + kSecAttrService: bundleIdentifier, + kSecAttrAccount: "\(bundleIdentifier).\(key)" ] - + let status = SecItemDelete(query as CFDictionary) - + if status != errSecSuccess { printLog("Error removing key from Keychain: \(status)") } diff --git a/MoppApp/MoppApp/LocalizationKeys.swift b/MoppApp/MoppApp/LocalizationKeys.swift index 2f726f944..e41e880ad 100644 --- a/MoppApp/MoppApp/LocalizationKeys.swift +++ b/MoppApp/MoppApp/LocalizationKeys.swift @@ -166,6 +166,8 @@ enum LocKey : String case smartIdCountryLithuania = "smart-id-country-lithuania" case smartIdCountryLatvia = "smart-id-country-latvia" case smartIdChallengeTitle = "smart-id-challenge-title" + case nfcTitle = "nfc-title" + case nfcCANTitle = "nfc-can-title" case containerSignTitle = "container-sign-title" case containerEncryptionTitle = "container-encryption-title" case containerDecryptionTitle = "container-decryption-title" @@ -190,6 +192,7 @@ enum LocKey : String case signTitleMobileId = "sign-title-mobile-id" case signTitleSmartId = "sign-title-smart-id" case signTitleIdCard = "sign-title-id-card" + case signTitleNFC = "sign-title-nfc" case cardReaderStateReaderNotFound = "card-reader-state-reader-not-found" case cardReaderStateReaderRestarted = "card-reader-state-reader-restarted" case cardReaderStateReaderProcessFailed = "card-reader-state-reader-process-failed" diff --git a/MoppApp/MoppApp/MoppApp.entitlements b/MoppApp/MoppApp/MoppApp.entitlements index 35496166c..a713e19e8 100644 --- a/MoppApp/MoppApp/MoppApp.entitlements +++ b/MoppApp/MoppApp/MoppApp.entitlements @@ -2,6 +2,10 @@ + com.apple.developer.nfc.readersession.formats + + TAG + com.apple.security.application-groups group.ee.ria.digidoc.ios diff --git a/MoppApp/MoppApp/NFCEditViewController.swift b/MoppApp/MoppApp/NFCEditViewController.swift new file mode 100644 index 000000000..f584d5749 --- /dev/null +++ b/MoppApp/MoppApp/NFCEditViewController.swift @@ -0,0 +1,222 @@ +// +// NFCEditViewController.swift +// MoppApp +// +/* + * Copyright 2017 - 2024 Riigi Infosüsteemi Amet + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ +import Foundation +import UIKit + +protocol NFCEditViewControllerDelegate : AnyObject { + func nfcEditViewControllerDidDismiss(cancelled: Bool, can: String?, pin: String?) +} + +class NFCEditViewController : MoppViewController, TokenFlowSigning { + @IBOutlet weak var scrollView: UIScrollView! + @IBOutlet weak var canTextField: UITextField! + @IBOutlet weak var pinTextField: UITextField! + @IBOutlet weak var titleLabel: UILabel! + @IBOutlet weak var canTextLabel: UILabel! + @IBOutlet weak var pinTextLabel: UILabel! + @IBOutlet weak var canTextErrorLabel: UILabel! + @IBOutlet weak var pinTextErrorLabel: UILabel! + @IBOutlet weak var cancelButton: MoppButton! + @IBOutlet weak var signButton: MoppButton! + + static private let nfcCANKey = "nfcCANKey" + weak var delegate: NFCEditViewControllerDelegate? = nil + var tapGR: UITapGestureRecognizer! + + override func viewDidLoad() { + super.viewDidLoad() + + titleLabel.text = L(.nfcTitle) + canTextLabel.text = L(.nfcCANTitle) + pinTextLabel.text = L(.pin2TextfieldLabel) + + canTextErrorLabel.text = "" + canTextErrorLabel.isHidden = true + pinTextErrorLabel.text = "" + pinTextErrorLabel.isHidden = true + + canTextField.moppPresentDismissButton() + canTextField.layer.borderColor = UIColor.moppContentLine.cgColor + canTextField.layer.borderWidth = 1.0 + canTextField.delegate = self + pinTextField.moppPresentDismissButton() + pinTextField.layer.borderColor = UIColor.moppContentLine.cgColor + pinTextField.layer.borderWidth = 1.0 + pinTextField.delegate = self + + cancelButton.setTitle(L(.actionCancel).uppercased()) + cancelButton.adjustedFont() + signButton.setTitle(L(.actionSign).uppercased()) + signButton.adjustedFont() + + tapGR = UITapGestureRecognizer() + tapGR.addTarget(self, action: #selector(cancelAction)) + view.addGestureRecognizer(tapGR) + } + + @objc func dismissKeyboard(_ notification: NSNotification) { + pinTextField.resignFirstResponder() + canTextField.resignFirstResponder() + } + + @IBAction func cancelAction() { + dismiss(animated: false) { + [weak self] in + guard let sself = self else { return } + sself.delegate?.nfcEditViewControllerDidDismiss( + cancelled: true, + can: nil, + pin: nil) + UIAccessibility.post(notification: .screenChanged, argument: L(.signingCancelled)) + } + } + + @IBAction func signAction() { + if DefaultsHelper.isRoleAndAddressEnabled { + let roleAndAddressView = UIStoryboard.tokenFlow.instantiateViewController(of: RoleAndAddressViewController.self) + roleAndAddressView.modalPresentationStyle = .overCurrentContext + roleAndAddressView.modalTransitionStyle = .crossDissolve + roleAndAddressView.viewController = self + present(roleAndAddressView, animated: true) + } else { + sign(nil) + } + } + + func sign(_ pin: String?) { + dismiss(animated: false) { + [weak self] in + guard let sself = self else { return } + sself.delegate?.nfcEditViewControllerDidDismiss( + cancelled: false, + can: sself.canTextField.text, + pin: sself.pinTextField.text) + } + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + canTextField.text = KeychainUtil.retrieve(key: NFCEditViewController.nfcCANKey) + pinTextField.text = "" + + canTextField.attributedPlaceholder = NSAttributedString(string: "CAN", attributes: [NSAttributedString.Key.foregroundColor: UIColor.moppPlaceholderDarker]) + pinTextField.attributedPlaceholder = NSAttributedString(string: "PIN2", attributes: [NSAttributedString.Key.foregroundColor: UIColor.moppPlaceholderDarker]) + + canTextField.addTarget(self, action: #selector(editingChanged(sender:)), for: .editingChanged) + pinTextField.addTarget(self, action: #selector(editingChanged(sender:)), for: .editingChanged) + + verifySigningCapability() + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + self.view.backgroundColor = UIColor.black.withAlphaComponent(0.0) + } + + deinit { + canTextField.removeTarget(self, action: #selector(editingChanged(sender:)), for: .editingChanged) + pinTextField.removeTarget(self, action: #selector(editingChanged(sender:)), for: .editingChanged) + } + + @objc func handleAccessibilityKeyboard(_ notification: NSNotification) { + dismissKeyboard(notification) + ViewUtil.focusOnView(notification, mainView: self.view, scrollView: scrollView) + } + + func verifySigningCapability() { + if canTextField.text?.count == 6, + pinTextField.text?.count ?? 0 >= 5 { + signButton.isEnabled = true + signButton.backgroundColor = UIColor.moppBase + } else { + signButton.isEnabled = false + signButton.backgroundColor = UIColor.moppLabel + } + } + + @objc func editingChanged(sender: UITextField) { + verifySigningCapability() + } + + override func keyboardWillShow(notification: NSNotification) { + if canTextField.isFirstResponder { + showKeyboard(textFieldLabel: canTextErrorLabel, scrollView: scrollView) + } + + if pinTextField.isFirstResponder { + showKeyboard(textFieldLabel: pinTextErrorLabel, scrollView: scrollView) + } + } + + override func keyboardWillHide(notification: NSNotification) { + hideKeyboard(scrollView: scrollView) + } +} + +extension NFCEditViewController : UITextFieldDelegate { + func textFieldShouldReturn(_ textField: UITextField) -> Bool { + guard textField.accessibilityIdentifier == "nfcCanField" else { + textField.resignFirstResponder() + return true + } + textField.resignFirstResponder() + if let pinTextField = getViewByAccessibilityIdentifier(view: view, identifier: "nfcPinField") { + pinTextField.becomeFirstResponder() + } + return true + } + + func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + if let text = textField.text as NSString? { + let textAfterUpdate = text.replacingCharacters(in: range, with: string) + return textAfterUpdate.isNumeric || textAfterUpdate.isEmpty + } + return true + } + + func textFieldDidBeginEditing(_ textField: UITextField) { + textField.moveCursorToEnd() + } + + func textFieldDidEndEditing(_ textField: UITextField) { + if textField.accessibilityIdentifier == "nfcCanField" { + if let can = textField.text { + _ = KeychainUtil.save(key: NFCEditViewController.nfcCANKey, info: can) + } else { + KeychainUtil.remove(key: NFCEditViewController.nfcCANKey) + canTextErrorLabel.text = "" + canTextErrorLabel.isHidden = true + removeViewBorder(view: textField) + UIAccessibility.post(notification: .layoutChanged, argument: canTextField) + } + } + if textField.accessibilityIdentifier == "nfcPinField", + textField.text != nil { + pinTextErrorLabel.text = "" + pinTextErrorLabel.isHidden = true + removeViewBorder(view: textField) + UIAccessibility.post(notification: .screenChanged, argument: pinTextField) + } + } +} diff --git a/MoppApp/MoppApp/NFCSignature.swift b/MoppApp/MoppApp/NFCSignature.swift new file mode 100644 index 000000000..fa09f3046 --- /dev/null +++ b/MoppApp/MoppApp/NFCSignature.swift @@ -0,0 +1,555 @@ +/* + * MoppApp - NFCSignature.swift + * Copyright 2017 - 2024 Riigi Infosüsteemi Amet + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +import Foundation +import CoreNFC +import CommonCrypto +import CryptoTokenKit +import SwiftECC +import BigInt +import SkSigningLib + +struct RuntimeError: Error { + let msg: String +} + +class NFCSignature : NSObject, NFCTagReaderSessionDelegate { + + static let shared: NFCSignature = NFCSignature() + var session: NFCTagReaderSession? + var CAN: String? + var PIN: String? + var containerPath: String? + var roleData: MoppLibRoleAddressData? + var ksEnc: Bytes? + var ksMac: Bytes? + var SSC: Bytes? + + func createNFCSignature(can: String, pin: String, containerPath: String, hashType: String, roleData: MoppLibRoleAddressData?) -> Void { + guard NFCTagReaderSession.readingAvailable else { + CancelUtil.handleCancelledRequest(errorMessageDetails: "This device doesn't support NFC.") + return + } + + CAN = can + PIN = pin + self.containerPath = containerPath + session = NFCTagReaderSession(pollingOption: .iso14443, delegate: self) + session?.alertMessage = "Hold your iPhone near the ID-Card." + session?.begin() + } + + // MARK: - NFCTagReaderSessionDelegate + + func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) { + if tags.count > 1 { + let retryInterval = DispatchTimeInterval.milliseconds(500) + session.alertMessage = "More than 1 tag is detected, please remove all tags and try again." + DispatchQueue.global().asyncAfter(deadline: .now() + retryInterval, execute: { + session.restartPolling() + }) + return + } + + guard case let .iso7816(tag) = tags.first else { + session.invalidate(errorMessage: "Invalid tag.") + return + } + + Task { + do { + try await session.connect(to: tags.first!) + } catch { + session.invalidate(errorMessage: "Unable to connect to tag.") + return + } + do { + session.alertMessage = "Authenticating with card." + if let (ksEnc, ksMac) = try await mutualAuthenticate(tag: tag) { + printLog("Mutual authentication successfull") + self.ksEnc = ksEnc + self.ksMac = ksMac + self.SSC = Bytes(repeating: 0x00, count: AES.BlockSize) + session.alertMessage = "Reading Certificate." + try await selectDF(tag: tag, file: Data()) + try await selectDF(tag: tag, file: Data([0xAD, 0xF2])) + let cert = try await readEF(tag: tag, file: Data([0x34, 0x1F])) + printLog("cert: \(cert.toHex)") + guard let hash = MoppLibManager.prepareSignature(cert.base64EncodedString(), containerPath: containerPath, roleData: roleData) else { + throw RuntimeError(msg: "Failed to prepare signature.") + } + session.alertMessage = "Sign document." + var pin = Data(repeating: 0xFF, count: 12) + pin.replaceSubrange(0.. (Bytes, Bytes)? { + let oid = "04007f00070202040204" // id-PACE-ECDH-GM-AES-CBC-CMAC-256 + // + CAN + _ = try await tag.sendCommand(cls: 0x00, ins: 0x22, p1: 0xc1, p2: 0xa4, data: Data(hex: "800a\(oid)830102")!, le: 256) + let nonceTag = try await tag.sendPaceCommand(records: [], tagExpected: 0x80) + printLog("Challenge \(nonceTag.data.toHex)") + let nonce = try decryptNonce(encryptedNonce: nonceTag.value) + printLog("Nonce \(nonce.toHex)") + let domain = Domain.instance(curve: .EC256r1) + + // Mapping data + let (terminalPubKey, terminalPrivKey) = domain.makeKeyPair() + let mappingTag = try await tag.sendPaceCommand(records: [try TKBERTLVRecord(tag: 0x81, publicKey: terminalPubKey)], tagExpected: 0x82) + printLog("Mapping key \(mappingTag.data.toHex)") + let cardPubKey = try ECPublicKey(domain: domain, tlv: mappingTag)! + + // Mapping + let nonceS = BInt(magnitude: nonce) + let mappingBasePoint = ECPublicKey(privateKey: try ECPrivateKey(domain: domain, s: nonceS)) // S*G + printLog("Card Key x: \(mappingBasePoint.w.x.asMagnitudeBytes().toHex), y: \(mappingBasePoint.w.y.asMagnitudeBytes().toHex)") + let sharedSecretH = try domain.multiplyPoint(cardPubKey.w, terminalPrivKey.s) + printLog("Shared Secret x: \(sharedSecretH.x.asMagnitudeBytes().toHex), y: \(sharedSecretH.y.asMagnitudeBytes().toHex)") + let mappedPoint = try domain.addPoints(mappingBasePoint.w, sharedSecretH) // MAP G = (S*G) + H + printLog("Mapped point x: \(mappedPoint.x.asMagnitudeBytes().toHex), y: \(mappedPoint.y.asMagnitudeBytes().toHex)") + let mappedDomain = try Domain.instance(name: domain.name + " Mapped", p: domain.p, a: domain.a, b: domain.b, gx: mappedPoint.x, gy: mappedPoint.y, order: domain.order, cofactor: domain.cofactor) + + // Ephemeral data + let (terminalEphemeralPubKey, terminalEphemeralPrivKey) = mappedDomain.makeKeyPair() + let ephemeralTag = try await tag.sendPaceCommand(records: [try TKBERTLVRecord(tag: 0x83, publicKey: terminalEphemeralPubKey)], tagExpected: 0x84) + printLog("Card Ephermal key \(ephemeralTag.data.toHex)") + let ephemeralCardPubKey = try ECPublicKey(domain: mappedDomain, tlv: ephemeralTag)! + + // Derive shared secret and session keys + let sharedSecret = try terminalEphemeralPrivKey.sharedSecret(pubKey: ephemeralCardPubKey) + printLog("Shared secret \(sharedSecret.toHex)") + let ksEnc = KDF(key: sharedSecret, counter: 1) + let ksMac = KDF(key: sharedSecret, counter: 2) + printLog("KS.Enc \(ksEnc.toHex)") + printLog("KS.Mac \(ksMac.toHex)") + + // Mutual authentication + let macHeader = Bytes(hex: "7f494f060a\(oid)8641")! + let macCalc = try AES.CMAC(key: Bytes(ksMac)) + let ephemeralCardPubKeyBytes = try ephemeralCardPubKey.x963Representation() + let macTag = try await tag.sendPaceCommand(records: [TKBERTLVRecord(tag: 0x85, bytes: (try macCalc.authenticate(bytes: macHeader + ephemeralCardPubKeyBytes, count: 8)))], tagExpected: 0x86) + printLog("Mac response \(macTag.data.toHex)") + + // verify chip's MAC and return session keys + let terminalEphemeralPubKeyBytes = try terminalEphemeralPubKey.x963Representation() + if macTag.value == Data(try macCalc.authenticate(bytes: macHeader + terminalEphemeralPubKeyBytes, count: 8)) { + return (ksEnc, ksMac) + } + return nil + } + + func sendWrapped(tag: NFCISO7816Tag, cls: UInt8, ins: UInt8, p1: UInt8, p2: UInt8, data: Data, le: Int) async throws -> Data { + guard SSC != nil else { + return try await tag.sendCommand(cls: cls, ins: ins, p1: p1, p2: p2, data: Data(), le: 256) + } + _ = SSC!.increment() + let DO87: Data + if !data.isEmpty { + let iv = try AES.CBC(key: ksEnc!, iv: AES.Zero).encrypt(SSC!) + let enc_data = try AES.CBC(key: ksEnc!, iv: iv).encrypt(data.addPadding()) + DO87 = TKBERTLVRecord(tag: 0x87, bytes: [0x01] + enc_data).data + } else { + DO87 = Data() + } + let DO97: Data + if le > 0 { + DO97 = TKBERTLVRecord(tag: 0x97, bytes: [UInt8(le == 256 ? 0 : le)]).data + } else { + DO97 = Data() + } + let cmd_header: Bytes = [cls | 0x0C, ins, p1, p2] + let M = cmd_header.addPadding() + DO87 + DO97 + let N = SSC! + M + let mac = try AES.CMAC(key: ksMac!).authenticate(bytes: N.addPadding(), count: 8) + let DO8E = TKBERTLVRecord(tag: 0x8E, bytes: mac).data + let send = DO87 + DO97 + DO8E + printLog(">: \(send.toHex)") + let response = try await tag.sendCommand(cls: cmd_header[0], ins: ins, p1: p1, p2: p2, data: send, le: 256) + printLog("<: \(response.toHex)") + var tlvEnc: TKTLVRecord? + var tlvRes: TKTLVRecord? + var tlvMac: TKTLVRecord? + for tlv in TKBERTLVRecord.sequenceOfRecords(from: response)! { + switch tlv.tag { + case 0x87: tlvEnc = tlv + case 0x99: tlvRes = tlv + case 0x8E: tlvMac = tlv + default: printLog("Unknown tag") + } + } + guard tlvRes != nil else { + throw RuntimeError(msg: "Missing RES tag") + } + guard tlvMac != nil else { + throw RuntimeError(msg: "Missing MAC tag") + } + let K = SSC!.increment() + (tlvEnc?.data ?? Data()) + tlvRes!.data + if try Data(AES.CMAC(key: ksMac!).authenticate(bytes: K.addPadding(), count: 8)) != tlvMac!.value { + throw RuntimeError(msg: "Invalid MAC value") + } + if tlvRes!.value != Data([0x90, 0x00]) { + throw RuntimeError(msg: "\(tlvRes!.value.toHex)") + } + guard tlvEnc != nil else { + return Data() + } + let iv = try AES.CBC(key: ksEnc!, iv: AES.Zero).encrypt(SSC!) + let responseData = try AES.CBC(key: ksEnc!, iv: iv).decrypt(tlvEnc!.value[1...]) + return Data(try responseData.removePadding()) + } + + func selectDF(tag: NFCISO7816Tag, file: Data) async throws { + _ = try await sendWrapped(tag: tag, cls: 0x00, ins: 0xA4, p1: file.isEmpty ? 0x00 : 0x01, p2: 0x0C, data: file, le: 256) + } + + func selectEF(tag: NFCISO7816Tag, file: Data) async throws -> Int { + let data = try await sendWrapped(tag: tag, cls: 0x00, ins: 0xA4, p1: 0x02, p2: 0x04, data: file, le: 256) + printLog("FCI: \(data.toHex)") + guard let fci = TKBERTLVRecord(from: data) else { + return 0 + } + for tlv in TKBERTLVRecord.sequenceOfRecords(from: fci.value)! where tlv.tag == 0x80 { + return Int(tlv.value[0]) << 8 | Int(tlv.value[1]) + } + return 0 + } + + func readBinary(tag: NFCISO7816Tag, len: Int, pos: Int) async throws -> Data { + return try await sendWrapped(tag: tag, cls: 0x00, ins: 0xB0, p1: UInt8(pos >> 8), p2: UInt8(truncatingIfNeeded: pos), data: Data(), le: len) + } + + func readBinary(tag: NFCISO7816Tag, len: Int) async throws -> Data { + var data = Data() + for i in stride(from: 0, to: len, by: 0xD8) { + data += try await readBinary(tag: tag, len: Swift.min(len - i, 0xD8), pos: i) + } + return data + } + + func readEF(tag: NFCISO7816Tag, file: Data) async throws -> Data { + let len = try await selectEF(tag: tag, file: file) + return try await readBinary(tag: tag, len: len) + } + + // MARK: - Utils + + private func decryptNonce(encryptedNonce: Data) throws -> Bytes { + let decryptionKey = KDF(key: Array(CAN!.utf8), counter: 3) + let cipher = AES.CBC(key: decryptionKey, iv: AES.Zero) + return try cipher.decrypt(Bytes(encryptedNonce)) + } + + private func KDF(key: Bytes, counter: UInt8) -> Bytes { + var keydata = key + Bytes(repeating: 0x00, count: 4) + keydata[keydata.count - 1] = counter + return SHA256(data: keydata) + } + + private func SHA256(data: Bytes) -> Bytes { + var hash = Bytes(repeating: 0x00, count: Int(CC_SHA256_DIGEST_LENGTH)) + _ = data.withUnsafeBytes { bufferPointer in + CC_SHA256(bufferPointer.baseAddress, CC_LONG(data.count), &hash) + } + return hash + } +} + + +// MARK: - Extensions + +extension DataProtocol where Self.Index == Int { + var toHex: String { + return map { String(format: "%02x", $0) }.joined() + } + + func chunked(into size: Int) -> [Bytes] { + return stride(from: 0, to: count, by: size).map { + Bytes(self[$0 ..< Swift.min($0 + size, count)]) + } + } + + func removePadding() throws -> SubSequence { + for i in (0.. Self { + var padding = Self(repeating: 0x00, count: AES.BlockSize - count % AES.BlockSize) + padding[0] = 0x80 + return self + padding + } + + public static func ^ (x: Self, y: Self) -> Self { + var result = x + for i in 0.. Self { + for i in (0.. Self { + var shifted = Self(repeating: 0x00, count: count) + let last = count - 1 + for index in 0.. Bytes { + return try domain.encodePoint(w) + } +} + +extension NFCISO7816Tag { + func sendCommand(cls: UInt8, ins: UInt8, p1: UInt8, p2: UInt8, data: Data, le: Int) async throws -> Data { + let apdu = NFCISO7816APDU(instructionClass: cls, instructionCode: ins, p1Parameter: p1, p2Parameter: p2, data: data, expectedResponseLength: le) + switch try await sendCommand(apdu: apdu) { + case (let data, 0x90, 0x00): + return data + case (let data, 0x61, let len): + return data + (try await sendCommand(cls: 0x00, ins: 0xC0, p1: 0x00, p2: 0x00, data: Data(), le: Int(len))) + case (_, 0x6C, let len): + return try await sendCommand(cls: cls, ins: ins, p1: p1, p2: p2, data: data, le: Int(len)) + case (_, let sw1, let sw2): + throw RuntimeError(msg: String(format: "%02X%02X", sw1, sw2)) + } + } + + func sendPaceCommand(records: [TKTLVRecord], tagExpected: TKTLVTag) async throws -> TKBERTLVRecord { + let request = TKBERTLVRecord(tag: 0x7c, records: records) + let data = try await sendCommand(cls: tagExpected == 0x86 ? 0x00 : 0x10, ins: 0x86, p1: 0x00, p2: 0x00, data: request.data, le: 256) + if let response = TKBERTLVRecord(from: data), response.tag == 0x7c, + let result = TKBERTLVRecord(from: response.value), result.tag == tagExpected { + return result + } + throw RuntimeError(msg: "Invalid response") + } +} + +extension TKBERTLVRecord { + convenience init(tag: TKTLVTag, bytes: Bytes) { + self.init(tag: tag, value: Data(bytes)) + } + + convenience init(tag: TKTLVTag, publicKey: ECPublicKey) throws { + self.init(tag: tag, bytes: (try publicKey.x963Representation())) + } +} + +// MARK: - AES +class AES { + static let BlockSize: Int = kCCBlockSizeAES128 + static let Zero = Bytes(repeating: 0x00, count: BlockSize) + + public class CBC { + private let key: Bytes + private let iv: Bytes + + init(key: Bytes, iv: Bytes) { + self.key = key + self.iv = iv + } + + func encrypt(_ data: T) throws -> Bytes { + return try crypt(data: data, operation: kCCEncrypt) + } + + func decrypt(_ data: T) throws -> Bytes { + return try crypt(data: data, operation: kCCDecrypt) + } + + private func crypt(data: T, operation: Int) throws -> Bytes { + var bytesWritten = 0 + var outputBuffer = Bytes(repeating: 0, count: data.count + BlockSize) + let status = data.withUnsafeBytes { dataBytes in + iv.withUnsafeBytes { ivBytes in + key.withUnsafeBytes { keyBytes in + CCCrypt( + CCOperation(operation), + CCAlgorithm(kCCAlgorithmAES), + CCOptions(0), //kCCOptionPKCS7Padding), + keyBytes.baseAddress, + key.count, + ivBytes.baseAddress, + dataBytes.baseAddress, + data.count, + &outputBuffer, + outputBuffer.count, + &bytesWritten + ) + } + } + } + if status != kCCSuccess { + throw RuntimeError(msg: "AES.CBC.Error") + } + return Bytes(outputBuffer.prefix(bytesWritten)) + } + } + + public class CMAC { + static let Rb: Bytes = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x87] + let cipher: AES.CBC + let K1: Bytes + let K2: Bytes + + public init(key: Bytes) throws { + cipher = AES.CBC(key: key, iv: Zero) + let L = try cipher.encrypt(Zero) + K1 = (L[0] & 0x80) == 0 ? L.leftShiftOneBit() : L.leftShiftOneBit() ^ CMAC.Rb + K2 = (K1[0] & 0x80) == 0 ? K1.leftShiftOneBit() : K1.leftShiftOneBit() ^ CMAC.Rb + } + + public func authenticate(bytes: T, count: Int) throws -> Bytes where T : DataProtocol, T.Index == Int { + let n = ceil(Double(bytes.count) / Double(BlockSize)) + let lastBlockComplete: Bool + if n == 0 { + lastBlockComplete = false + } else { + lastBlockComplete = bytes.count % BlockSize == 0 + } + + var blocks = bytes.chunked(into: BlockSize) + var M_last = blocks.popLast() ?? Bytes() + if lastBlockComplete { + M_last = M_last ^ K1 + } else { + M_last = M_last.addPadding() ^ K2 + } + + var x = Bytes(repeating: 0x00, count: BlockSize) + var y: Bytes + for M_i in blocks { + y = x ^ M_i + x = try cipher.encrypt(y) + } + y = M_last ^ x + let T = try cipher.encrypt(y) + return Bytes(T[0.. + @@ -161,6 +173,7 @@ + @@ -174,6 +187,7 @@ + @@ -1388,6 +1402,211 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MoppApp/MoppApp/TokenFlowSelectionViewController.swift b/MoppApp/MoppApp/TokenFlowSelectionViewController.swift index 99de52877..f57eea0a4 100644 --- a/MoppApp/MoppApp/TokenFlowSelectionViewController.swift +++ b/MoppApp/MoppApp/TokenFlowSelectionViewController.swift @@ -36,7 +36,8 @@ class TokenFlowSelectionViewController : MoppViewController { @IBOutlet weak var mobileIDButton: ScaledButton! @IBOutlet weak var smartIDButton: ScaledButton! @IBOutlet weak var idCardButton: ScaledButton! - + @IBOutlet weak var nfcButton: ScaledButton! + @IBOutlet weak var tokenViewContainerTopConstraint: NSLayoutConstraint! @IBOutlet weak var tokenFlowViewLeadingCSTR: NSLayoutConstraint! @IBOutlet weak var tokenFlowViewTrailingCSTR: NSLayoutConstraint! @@ -51,7 +52,8 @@ class TokenFlowSelectionViewController : MoppViewController { weak var smartIdEditViewControllerDelegate: SmartIDEditViewControllerDelegate! weak var idCardSignViewControllerDelegate: IdCardSignViewControllerDelegate? weak var idCardDecryptViewControllerDelegate: IdCardDecryptViewControllerDelegate? - + weak var nfcEditViewControllerDelegate: NFCEditViewControllerDelegate! + var containerPath: String! var isSwitchingBlockedByTransition: Bool = false @@ -71,6 +73,7 @@ class TokenFlowSelectionViewController : MoppViewController { case mobileID case smartID case idCard + case nfc } override func viewDidLoad() { @@ -127,19 +130,24 @@ class TokenFlowSelectionViewController : MoppViewController { switch id { case .idCard: $0.setTitle(L(.signTitleIdCard)) - idCardButton.accessibilityLabel = setTabAccessibilityLabel(isTabSelected: false, tabName: L(.signTitleIdCard), positionInRow: "3", viewCount: "3") + idCardButton.accessibilityLabel = setTabAccessibilityLabel(isTabSelected: false, tabName: L(.signTitleIdCard), positionInRow: "3", viewCount: "4") idCardButton.accessibilityUserInputLabels = [L(.voiceControlIdCard)] idCardButton.adjustedFont() case .mobileID: $0.setTitle(L(.signTitleMobileId)) - mobileIDButton.accessibilityLabel = setTabAccessibilityLabel(isTabSelected: false, tabName: L(.signTitleMobileId), positionInRow: "1", viewCount: "3") + mobileIDButton.accessibilityLabel = setTabAccessibilityLabel(isTabSelected: false, tabName: L(.signTitleMobileId), positionInRow: "1", viewCount: "4") mobileIDButton.accessibilityUserInputLabels = [L(.voiceControlMobileId)] mobileIDButton.adjustedFont() case .smartID: $0.setTitle(L(.signTitleSmartId)) - smartIDButton.accessibilityLabel = setTabAccessibilityLabel(isTabSelected: false, tabName: L(.signTitleSmartId), positionInRow: "2", viewCount: "3") + smartIDButton.accessibilityLabel = setTabAccessibilityLabel(isTabSelected: false, tabName: L(.signTitleSmartId), positionInRow: "2", viewCount: "4") smartIDButton.accessibilityUserInputLabels = [L(.voiceControlSmartId)] smartIDButton.adjustedFont() + case .nfc: + $0.setTitle(L(.signTitleNFC)) + nfcButton.accessibilityLabel = setTabAccessibilityLabel(isTabSelected: false, tabName: L(.signTitleNFC), positionInRow: "4", viewCount: "4") + nfcButton.accessibilityUserInputLabels = [L(.signTitleNFC)] + nfcButton.adjustedFont() } } } @@ -191,19 +199,25 @@ extension TokenFlowSelectionViewController { } idCardSignVC.keyboardDelegate = self newViewController = idCardSignVC - viewAccessibilityElements = [idCardButton, containerView, mobileIDButton, smartIDButton, containerView] + viewAccessibilityElements = [idCardButton, containerView, mobileIDButton, smartIDButton, nfcButton, containerView] + case .nfc: + let nfcSignVC = UIStoryboard.tokenFlow.instantiateViewController(of: NFCEditViewController.self) + centerLandscapeCSTR.isActive = false + nfcSignVC.delegate = nfcEditViewControllerDelegate + newViewController = nfcSignVC + viewAccessibilityElements = [nfcButton, containerView, idCardButton, mobileIDButton, smartIDButton, containerView] case .mobileID: let mobileIdEditVC = UIStoryboard.tokenFlow.instantiateViewController(of: MobileIDEditViewController.self) handleConstraintInLandscape() mobileIdEditVC.delegate = mobileIdEditViewControllerDelegate newViewController = mobileIdEditVC - viewAccessibilityElements = [mobileIDButton, containerView, smartIDButton, idCardButton, containerView] + viewAccessibilityElements = [mobileIDButton, containerView, smartIDButton, idCardButton, nfcButton, containerView] case .smartID: let smartIdEditVC = UIStoryboard.tokenFlow.instantiateViewController(of: SmartIDEditViewController.self) handleConstraintInLandscape() smartIdEditVC.delegate = smartIdEditViewControllerDelegate newViewController = smartIdEditVC - viewAccessibilityElements = [smartIDButton, containerView, idCardButton, mobileIDButton, smartIDButton, containerView] + viewAccessibilityElements = [smartIDButton, containerView, idCardButton, mobileIDButton, smartIDButton, nfcButton, containerView] } if UIAccessibility.isVoiceOverRunning { diff --git a/MoppApp/MoppApp/en.lproj/Localizable.strings b/MoppApp/MoppApp/en.lproj/Localizable.strings index e350c876d..02ddfa23c 100755 --- a/MoppApp/MoppApp/en.lproj/Localizable.strings +++ b/MoppApp/MoppApp/en.lproj/Localizable.strings @@ -230,6 +230,8 @@ "smart-id-country-latvia" = "Latvia"; "smart-id-challenge-title" = "Sign with Smart-ID"; "smart-id-sign-help-title" = "Make sure control code matches with one in phone screen and enter Smart-ID PIN2 code"; +"nfc-title" = "Enter your card access number and PIN 2 to sign with ID-card"; +"nfc-can-title" = "Card access number"; "role-and-address" = "Role and address"; "role-and-address-title" = "Enter your role and address info"; "role-and-address-role-title" = "Role / resolution"; @@ -253,6 +255,7 @@ "sign-title-mobile-id" = "Mobile-ID"; "sign-title-smart-id" = "Smart-ID"; "sign-title-id-card" = "ID-Card"; +"sign-title-nfc" = "NFC"; "card-reader-state-initial" = "Connect the card reader and insert your ID-card into the reader."; "card-reader-state-reader-not-found" = "No card readers found"; "card-reader-state-reader-restarted" = "Restarting card reader."; diff --git a/MoppApp/MoppApp/et.lproj/Localizable.strings b/MoppApp/MoppApp/et.lproj/Localizable.strings index 36bea1ccd..9ec043981 100755 --- a/MoppApp/MoppApp/et.lproj/Localizable.strings +++ b/MoppApp/MoppApp/et.lproj/Localizable.strings @@ -231,6 +231,8 @@ "smart-id-country-latvia" = "Läti"; "smart-id-challenge-title" = "Allkirjasta Smart-IDga"; "smart-id-sign-help-title" = "Veendu kontrollkoodi õigsuses ja sisesta nutiseadmes Smart-ID PIN2-kood"; +"nfc-title" = "Sisesta oma kaardi ligipääsu number ja PIN 2 kood ID-kaardiga allkirjastamiseks"; +"nfc-can-title" = "Kaardi ligipääsu number"; "role-and-address" = "Roll ja aadress"; "role-and-address-title" = "Sisesta oma roll ja aadress"; "role-and-address-role-title" = "Roll / resolutsioon"; @@ -254,6 +256,7 @@ "sign-title-mobile-id" = "Mobiil-ID"; "sign-title-smart-id" = "Smart-ID"; "sign-title-id-card" = "ID-kaart"; +"sign-title-nfc" = "NFC"; "card-reader-state-reader-not-found" = "Ühtegi kaardilugejat pole ühendatud"; "card-reader-state-reader-restarted" = "Taaskäivitan kaardilugeja."; "card-reader-state-reader-process-failed" = "Andmete lugemine ebaõnnestus. Palun taasühenda kaardilugeja ja proovi uuesti."; diff --git a/MoppApp/MoppApp/ru.lproj/Localizable.strings b/MoppApp/MoppApp/ru.lproj/Localizable.strings index bc2f43506..6cef862c1 100644 --- a/MoppApp/MoppApp/ru.lproj/Localizable.strings +++ b/MoppApp/MoppApp/ru.lproj/Localizable.strings @@ -231,6 +231,8 @@ "smart-id-country-latvia" = "Латвия"; "smart-id-challenge-title" = "Подписать с помощью приложения Smart-ID"; "smart-id-sign-help-title" = "Убедитесь в правильности контрольного кода и введите PIN2-код для Smart-ID"; +"nfc-title" = "Enter your card access number and PIN 2 to sign with ID-card"; +"nfc-can-title" = "Card access number"; "role-and-address" = "Роль и адрес"; "role-and-address-title" = "Введите свою роль и адрес"; "role-and-address-role-title" = "Роль / pезолюция"; @@ -254,6 +256,7 @@ "sign-title-mobile-id" = "Mobiil-ID"; "sign-title-smart-id" = "Smart-ID"; "sign-title-id-card" = "ID-карта"; +"sign-title-nfc" = "NFC"; "card-reader-state-initial" = "Подключите считыватель карты и вставьте в него Вашу ID-карту."; "card-reader-state-reader-not-found" = "Считыватель карты не обнаружен"; "card-reader-state-reader-restarted" = "Перезапуск устройства чтения карт."; diff --git a/SkSigningLib/SkSigningLib/Models/SigningType.swift b/SkSigningLib/SkSigningLib/Models/SigningType.swift index 9dcbcafe2..7502a5c75 100644 --- a/SkSigningLib/SkSigningLib/Models/SigningType.swift +++ b/SkSigningLib/SkSigningLib/Models/SigningType.swift @@ -27,4 +27,5 @@ public enum SigningType: String { case mobileId case smartId case idCard + case nfc } From e714d9b8d22a9faee1e00eec13f15057765acd21 Mon Sep 17 00:00:00 2001 From: Marten Rebane <54431068+martenrebane@users.noreply.github.com> Date: Wed, 6 Mar 2024 15:00:23 +0200 Subject: [PATCH 2/6] Accessibility support --- MoppApp/MoppApp/NFCEditViewController.swift | 5 +- MoppApp/MoppApp/NFCSignature.swift | 55 +++++++++++++++------ MoppApp/MoppApp/TokenFlow.storyboard | 52 ++++++++++--------- 3 files changed, 75 insertions(+), 37 deletions(-) diff --git a/MoppApp/MoppApp/NFCEditViewController.swift b/MoppApp/MoppApp/NFCEditViewController.swift index f584d5749..1c720790c 100644 --- a/MoppApp/MoppApp/NFCEditViewController.swift +++ b/MoppApp/MoppApp/NFCEditViewController.swift @@ -20,7 +20,7 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * */ -import Foundation + import UIKit protocol NFCEditViewControllerDelegate : AnyObject { @@ -125,6 +125,9 @@ class NFCEditViewController : MoppViewController, TokenFlowSigning { canTextField.addTarget(self, action: #selector(editingChanged(sender:)), for: .editingChanged) pinTextField.addTarget(self, action: #selector(editingChanged(sender:)), for: .editingChanged) + + canTextField.accessibilityLabel = L(.nfcCANTitle) + pinTextField.accessibilityLabel = L(.pin2TextfieldLabel) verifySigningCapability() } diff --git a/MoppApp/MoppApp/NFCSignature.swift b/MoppApp/MoppApp/NFCSignature.swift index fa09f3046..bc28e5686 100644 --- a/MoppApp/MoppApp/NFCSignature.swift +++ b/MoppApp/MoppApp/NFCSignature.swift @@ -54,6 +54,12 @@ class NFCSignature : NSObject, NFCTagReaderSessionDelegate { session = NFCTagReaderSession(pollingOption: .iso14443, delegate: self) session?.alertMessage = "Hold your iPhone near the ID-Card." session?.begin() + + if UIAccessibility.isVoiceOverRunning { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + UIAccessibility.post(notification: .announcement, argument: "Hold your iPhone near the ID-Card.") + } + } } // MARK: - NFCTagReaderSessionDelegate @@ -62,6 +68,9 @@ class NFCSignature : NSObject, NFCTagReaderSessionDelegate { if tags.count > 1 { let retryInterval = DispatchTimeInterval.milliseconds(500) session.alertMessage = "More than 1 tag is detected, please remove all tags and try again." + if UIAccessibility.isVoiceOverRunning { + UIAccessibility.post(notification: .announcement, argument: session.alertMessage) + } DispatchQueue.global().asyncAfter(deadline: .now() + retryInterval, execute: { session.restartPolling() }) @@ -70,6 +79,9 @@ class NFCSignature : NSObject, NFCTagReaderSessionDelegate { guard case let .iso7816(tag) = tags.first else { session.invalidate(errorMessage: "Invalid tag.") + if UIAccessibility.isVoiceOverRunning { + UIAccessibility.post(notification: .announcement, argument: "Invalid tag.") + } return } @@ -78,16 +90,26 @@ class NFCSignature : NSObject, NFCTagReaderSessionDelegate { try await session.connect(to: tags.first!) } catch { session.invalidate(errorMessage: "Unable to connect to tag.") + if UIAccessibility.isVoiceOverRunning { + UIAccessibility.post(notification: .announcement, argument: "Unable to connect to tag.") + } return } do { session.alertMessage = "Authenticating with card." + if UIAccessibility.isVoiceOverRunning { + UIAccessibility.post(notification: .announcement, argument: session.alertMessage) + } + if let (ksEnc, ksMac) = try await mutualAuthenticate(tag: tag) { - printLog("Mutual authentication successfull") + printLog("Mutual authentication successful") self.ksEnc = ksEnc self.ksMac = ksMac self.SSC = Bytes(repeating: 0x00, count: AES.BlockSize) session.alertMessage = "Reading Certificate." + if UIAccessibility.isVoiceOverRunning { + UIAccessibility.post(notification: .announcement, argument: session.alertMessage) + } try await selectDF(tag: tag, file: Data()) try await selectDF(tag: tag, file: Data([0xAD, 0xF2])) let cert = try await readEF(tag: tag, file: Data([0x34, 0x1F])) @@ -96,49 +118,51 @@ class NFCSignature : NSObject, NFCTagReaderSessionDelegate { throw RuntimeError(msg: "Failed to prepare signature.") } session.alertMessage = "Sign document." + if UIAccessibility.isVoiceOverRunning { + UIAccessibility.post(notification: .announcement, argument: session.alertMessage) + } var pin = Data(repeating: 0xFF, count: 12) pin.replaceSubrange(0.. - + - + @@ -1418,7 +1418,7 @@ - + - + @@ -1451,18 +1455,27 @@ + - + - + @@ -1476,14 +1489,14 @@ -