TFAPasswordVC.swift 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439
  1. //
  2. // TFAPasswordVC.swift
  3. // Pods
  4. //
  5. // Created by Qindi on 22/10/25.
  6. //
  7. import UIKit
  8. import Foundation
  9. import os
  10. import LocalAuthentication
  11. import nuSDKService
  12. class TFAPasswordVC: UIViewController {
  13. static let STEP_FIDO = "1";
  14. static let STEP_FIDO_PWD = "1,2";
  15. static let STEP_FIDO_PWD_BIOFINGER = "1,2,3";
  16. static let STEP_FIDO_PWD_BIOFACE = "1,2,4";
  17. static let STEP_FIDO_BIOFINGER = "1,3";
  18. static let STEP_FIDO_BIOFACE = "1,4";
  19. var STEP_NEEDED = STEP_FIDO_PWD
  20. var METHOD = "Sign In"
  21. private let imageViewBackground = UIImageView()
  22. private let scrollView = UIScrollView()
  23. private let mainStackView = UIStackView()
  24. private let headerImageView1 = UIImageView()
  25. private let headerImageView2 = UIImageView()
  26. private let headerTitleLabel = UILabel()
  27. private let subtitleLabel = UILabel()
  28. private let passwordTextField = UITextField()
  29. private let passwordVisibilityButton = UIButton(type: .system)
  30. private let poweredStackView = UIStackView()
  31. private let noteLabel = UILabel()
  32. private let poweredLabel = UILabel()
  33. private let poweredImageView = UIImageView()
  34. private var isPasswordVisible = false
  35. var isFromSU = false
  36. override func viewDidLoad() {
  37. super.viewDidLoad()
  38. if isFromSU {
  39. SecureUserDefaults.shared.removeValue(forKey: "lastAuthenticationTime")
  40. }
  41. navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Submit".localized(), style: .plain, target: self, action: #selector(submitAction))
  42. let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard))
  43. tapGesture.cancelsTouchesInView = false
  44. self.view.addGestureRecognizer(tapGesture)
  45. setupUI()
  46. setupLayout()
  47. loadData()
  48. updateUIBasedOnMethod()
  49. DispatchQueue.global().async {
  50. if Utils.isMiddleMode() && Utils.getBiometricState() != nil {
  51. self.biometricAuth()
  52. }
  53. }
  54. }
  55. override func viewWillAppear(_ animated: Bool) {
  56. let attributes = [NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 16.0), NSAttributedString.Key.foregroundColor: UIColor.white]
  57. let navBarAppearance = UINavigationBarAppearance()
  58. navBarAppearance.configureWithOpaqueBackground()
  59. navBarAppearance.backgroundColor = self.traitCollection.userInterfaceStyle == .dark ? .blackDarkMode : UIColor.mainColor
  60. navBarAppearance.titleTextAttributes = attributes
  61. navigationController?.navigationBar.standardAppearance = navBarAppearance
  62. navigationController?.navigationBar.scrollEdgeAppearance = navBarAppearance
  63. NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow),
  64. name: UIResponder.keyboardWillShowNotification, object: nil)
  65. NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide),
  66. name: UIResponder.keyboardWillHideNotification, object: nil)
  67. }
  68. override func viewWillDisappear(_ animated: Bool) {
  69. NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil)
  70. NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil)
  71. }
  72. @objc private func keyboardWillShow(notification: Notification) {
  73. guard let userInfo = notification.userInfo,
  74. let keyboardFrame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
  75. let keyboardHeight = keyboardFrame.height
  76. let bottomInset = keyboardHeight - view.safeAreaInsets.bottom
  77. scrollView.contentInset.bottom = bottomInset - 20
  78. scrollView.verticalScrollIndicatorInsets.bottom = bottomInset - 20
  79. // ✅ Scroll password field above keyboard
  80. let passwordFrameInScroll = scrollView.convert(passwordTextField.frame, from: passwordTextField.superview)
  81. scrollView.scrollRectToVisible(passwordFrameInScroll, animated: true)
  82. }
  83. @objc private func keyboardWillHide(notification: Notification) {
  84. scrollView.contentInset = .zero
  85. scrollView.verticalScrollIndicatorInsets = .zero
  86. }
  87. @objc func cancel(sender: Any) {
  88. navigationController?.dismiss(animated: true, completion: nil)
  89. }
  90. @objc func dismissKeyboard() {
  91. passwordTextField.resignFirstResponder()
  92. }
  93. // MARK: - UI Setup
  94. private func setupUI() {
  95. view.backgroundColor = .systemBackground
  96. // Background Image View
  97. imageViewBackground.contentMode = .scaleAspectFill
  98. imageViewBackground.translatesAutoresizingMaskIntoConstraints = false
  99. view.addSubview(imageViewBackground)
  100. // Scroll View
  101. scrollView.showsVerticalScrollIndicator = false
  102. scrollView.translatesAutoresizingMaskIntoConstraints = false
  103. view.addSubview(scrollView)
  104. // Main Stack View
  105. mainStackView.axis = .vertical
  106. mainStackView.alignment = .center
  107. mainStackView.spacing = 16
  108. mainStackView.translatesAutoresizingMaskIntoConstraints = false
  109. scrollView.addSubview(mainStackView)
  110. // Header Images
  111. headerImageView1.contentMode = .scaleAspectFit
  112. headerImageView1.image = UIImage(named: "pb_ic_attach_spc_badge", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)
  113. headerImageView1.heightAnchor.constraint(equalToConstant: 100).isActive = true
  114. mainStackView.addArrangedSubview(headerImageView1)
  115. headerImageView2.contentMode = .scaleAspectFit
  116. headerImageView2.image = UIImage(named: "pb_mfa_splash", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)
  117. headerImageView2.heightAnchor.constraint(equalToConstant: 200).isActive = true
  118. mainStackView.addArrangedSubview(headerImageView2)
  119. // Header Title Label
  120. headerTitleLabel.font = .boldSystemFont(ofSize: 17)
  121. headerTitleLabel.textAlignment = .center
  122. headerTitleLabel.numberOfLines = 0
  123. mainStackView.addArrangedSubview(headerTitleLabel)
  124. // Subtitle Label
  125. subtitleLabel.text = "Please input your password to continue"
  126. subtitleLabel.font = .systemFont(ofSize: 12)
  127. subtitleLabel.textAlignment = .center
  128. subtitleLabel.numberOfLines = 0
  129. mainStackView.addArrangedSubview(subtitleLabel)
  130. // Password Input Container
  131. let passwordContainerView = UIView()
  132. passwordContainerView.translatesAutoresizingMaskIntoConstraints = false
  133. passwordContainerView.widthAnchor.constraint(equalToConstant: 300).isActive = true
  134. passwordContainerView.heightAnchor.constraint(equalToConstant: 48).isActive = true
  135. mainStackView.addArrangedSubview(passwordContainerView)
  136. // Password Text Field
  137. passwordTextField.placeholder = "Type your password..."
  138. passwordTextField.isSecureTextEntry = true
  139. passwordTextField.font = .systemFont(ofSize: 15)
  140. passwordTextField.borderStyle = .roundedRect
  141. passwordTextField.keyboardType = .default
  142. passwordTextField.autocapitalizationType = .none
  143. passwordTextField.autocorrectionType = .no
  144. passwordTextField.translatesAutoresizingMaskIntoConstraints = false
  145. passwordContainerView.addSubview(passwordTextField)
  146. // Password Visibility Button
  147. passwordVisibilityButton.setImage(UIImage(systemName: "eye.slash.fill"), for: .normal)
  148. passwordVisibilityButton.addTarget(self, action: #selector(togglePasswordVisibility), for: .touchUpInside)
  149. passwordVisibilityButton.translatesAutoresizingMaskIntoConstraints = false
  150. passwordVisibilityButton.tintColor = .black
  151. passwordContainerView.addSubview(passwordVisibilityButton)
  152. if isFromSU {
  153. print("MASUK SINI GA?")
  154. noteLabel.attributedText = "_Note: If you have not changed the password provided by this user or are unsure of it, please note that the default password is *abcd1234*_".localized().richText()
  155. noteLabel.numberOfLines = 0
  156. mainStackView.addArrangedSubview(noteLabel)
  157. }
  158. mainStackView.setCustomSpacing(12, after: subtitleLabel)
  159. mainStackView.setCustomSpacing(24, after: passwordContainerView)
  160. // Powered By StackView
  161. poweredStackView.axis = .horizontal
  162. poweredStackView.alignment = .center
  163. poweredStackView.spacing = 8
  164. poweredStackView.translatesAutoresizingMaskIntoConstraints = false
  165. view.addSubview(poweredStackView)
  166. poweredLabel.text = "Powered by"
  167. poweredLabel.font = .systemFont(ofSize: 12)
  168. poweredStackView.addArrangedSubview(poweredLabel)
  169. poweredImageView.contentMode = .scaleAspectFit
  170. poweredImageView.image = UIImage(named: "pb_powered_button")
  171. poweredImageView.widthAnchor.constraint(equalToConstant: 25).isActive = true
  172. poweredImageView.heightAnchor.constraint(equalToConstant: 25).isActive = true
  173. poweredStackView.addArrangedSubview(poweredImageView)
  174. }
  175. private func setupLayout() {
  176. NSLayoutConstraint.activate([
  177. // Background
  178. imageViewBackground.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
  179. imageViewBackground.leadingAnchor.constraint(equalTo: view.leadingAnchor),
  180. imageViewBackground.trailingAnchor.constraint(equalTo: view.trailingAnchor),
  181. imageViewBackground.bottomAnchor.constraint(equalTo: view.bottomAnchor),
  182. // Scroll View
  183. scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
  184. scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 24),
  185. scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -24),
  186. scrollView.bottomAnchor.constraint(equalTo: poweredStackView.topAnchor, constant: -8),
  187. // Main Stack View
  188. mainStackView.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 20),
  189. mainStackView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
  190. mainStackView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
  191. mainStackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
  192. mainStackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
  193. // Password Field
  194. passwordTextField.leadingAnchor.constraint(equalTo: passwordTextField.superview!.leadingAnchor),
  195. passwordTextField.trailingAnchor.constraint(equalTo: passwordTextField.superview!.trailingAnchor),
  196. passwordTextField.topAnchor.constraint(equalTo: passwordTextField.superview!.topAnchor),
  197. passwordTextField.bottomAnchor.constraint(equalTo: passwordTextField.superview!.bottomAnchor),
  198. // Password Visibility Button
  199. passwordVisibilityButton.trailingAnchor.constraint(equalTo: passwordTextField.trailingAnchor, constant: -8),
  200. passwordVisibilityButton.centerYAnchor.constraint(equalTo: passwordTextField.centerYAnchor),
  201. passwordVisibilityButton.widthAnchor.constraint(equalToConstant: 40),
  202. passwordVisibilityButton.heightAnchor.constraint(equalToConstant: 40),
  203. // Powered by
  204. poweredStackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -16),
  205. poweredStackView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -8),
  206. ])
  207. }
  208. // MARK: - Data & Logic
  209. private func loadData() {
  210. let poweredText = "Nexilis"
  211. if !poweredText.isEmpty {
  212. poweredLabel.text = "Powered by \(poweredText)"
  213. }
  214. }
  215. // MARK: - Actions
  216. @objc private func togglePasswordVisibility() {
  217. isPasswordVisible.toggle()
  218. passwordTextField.isSecureTextEntry = !isPasswordVisible
  219. let iconName = isPasswordVisible ? "eye.fill" : "eye.slash.fill"
  220. passwordVisibilityButton.setImage(UIImage(systemName: iconName), for: .normal)
  221. }
  222. @objc private func submitAction() {
  223. guard let password = passwordTextField.text, !password.trimmingCharacters(in: .whitespaces).isEmpty else {
  224. self.view.makeToast("Password cannot be empty.".localized(), duration: 2.0, position: .center)
  225. return
  226. }
  227. guard password.count >= 6 else {
  228. self.view.makeToast("Password must be at least 6 characters.".localized(), duration: 2.0, position: .center)
  229. return
  230. }
  231. submit()
  232. }
  233. private func submit(fromBiometric: Bool = false) {
  234. guard let password = passwordTextField.text else { return }
  235. Nexilis.showLoader()
  236. DispatchQueue.global().async {
  237. do {
  238. // 1. Encrypt password
  239. let encryptedPwd = password
  240. // 2. Create message for the server
  241. let me = User.getMyPin() ?? ""
  242. let tMessage = CoreMessage_TMessageBank.getMFAValidation(data: me)
  243. if !fromBiometric {
  244. tMessage.mBodies[CoreMessage_TMessageKey.PSWD] = encryptedPwd
  245. }
  246. tMessage.mBodies[CoreMessage_TMessageKey.SUBMIT_DATE] = "\(Date().currentTimeMillis())"
  247. tMessage.mBodies[CoreMessage_TMessageKey.ACTVITY] = self.METHOD
  248. guard let privateKey = KeyManagerNexilis.getPrivateKey(useBiometric: Utils.isHSAMode()) else {
  249. DispatchQueue.main.async {
  250. Nexilis.hideLoader {
  251. let errorMessage = "Biometric Failed".localized()
  252. let dialog = DialogErrorMFA()
  253. dialog.modalTransitionStyle = .crossDissolve
  254. dialog.modalPresentationStyle = .overCurrentContext
  255. dialog.errorDesc = errorMessage
  256. dialog.method = self.METHOD
  257. UIApplication.shared.visibleViewController?.present(dialog, animated: true)
  258. }
  259. }
  260. return
  261. }
  262. var id = ""
  263. if Utils.isMiddleMode() || Utils.isHSAMode() {
  264. id = Nexilis.justInit()
  265. } else {
  266. id = User.getMyPin() ?? ""
  267. }
  268. if let response = Nexilis.writeAndWait(message: CoreMessage_TMessageBank.getChalanger(xPin: id)) {
  269. if response.isOk() {
  270. let data = response.getBody(key: CoreMessage_TMessageKey.DATA, default_value: "")
  271. if data.isEmpty {
  272. DispatchQueue.main.async {
  273. Nexilis.hideLoader {
  274. let errorMessage = "Auth Failure".localized()
  275. let dialog = DialogErrorMFA()
  276. dialog.modalTransitionStyle = .crossDissolve
  277. dialog.modalPresentationStyle = .overCurrentContext
  278. dialog.errorDesc = errorMessage
  279. dialog.method = self.METHOD
  280. UIApplication.shared.visibleViewController?.present(dialog, animated: true)
  281. }
  282. }
  283. return
  284. }
  285. let df = HMACDeviceFingerprintNexilis.generate()
  286. tMessage.mBodies[CoreMessage_TMessageKey.FINGERPRINT] = df
  287. var sign = ""
  288. if let dataSign = "\(data)!\(df)".data(using: .utf8) {
  289. if let signature = KeyManagerNexilis.sign(data: dataSign, privateKey: privateKey) {
  290. sign = signature.base64EncodedString()
  291. }
  292. }
  293. tMessage.mBodies[CoreMessage_TMessageKey.SIGNATURE] = sign
  294. let otp = try TOTPGenerator.generateTOTP(base32Secret: TOTPGenerator.getTOTP(), digits: 6, timeStepSeconds: 300)
  295. tMessage.mBodies[CoreMessage_TMessageKey.TOTP] = otp
  296. if let response = Nexilis.writeAndWait(message: tMessage) {
  297. if response.isOk() {
  298. if Utils.isMiddleMode() && Utils.getBiometricState() == nil {
  299. SecureUserDefaults.shared.set(Date(), forKey: "lastAuthenticationTime")
  300. }
  301. Nexilis.setInitCallback() { res in
  302. if res == 1 {
  303. Nexilis.successSui?()
  304. closePage()
  305. }
  306. }
  307. Nexilis.startConnect(withInit: false)
  308. func closePage() {
  309. DispatchQueue.main.async {
  310. Nexilis.hideLoader {
  311. self.navigationController?.dismiss(animated: true, completion: {
  312. UIApplication.shared.visibleViewController?.view.makeToast("Successfully Authenticated".localized(), duration: 3)
  313. self.dismissKeyboard()
  314. })
  315. }
  316. }
  317. }
  318. }
  319. else {
  320. DispatchQueue.main.async {
  321. Nexilis.hideLoader {
  322. let errorMessage = response.getBody(key: CoreMessage_TMessageKey.MESSAGE_TEXT, default_value: "Auth Failure".localized())
  323. let dialog = DialogErrorMFA()
  324. dialog.modalTransitionStyle = .crossDissolve
  325. dialog.modalPresentationStyle = .overCurrentContext
  326. dialog.errorDesc = errorMessage
  327. dialog.method = self.METHOD
  328. UIApplication.shared.visibleViewController?.present(dialog, animated: true)
  329. }
  330. }
  331. }
  332. }
  333. }
  334. } else {
  335. DispatchQueue.main.async {
  336. Nexilis.hideLoader {
  337. let errorMessage = "Unable to access servers. Check your internet connection and try again later".localized()
  338. let dialog = DialogErrorMFA()
  339. dialog.modalTransitionStyle = .crossDissolve
  340. dialog.modalPresentationStyle = .overCurrentContext
  341. dialog.errorDesc = errorMessage
  342. dialog.method = self.METHOD
  343. UIApplication.shared.visibleViewController?.present(dialog, animated: true)
  344. }
  345. }
  346. }
  347. } catch {
  348. }
  349. }
  350. }
  351. private func biometricAuth() {
  352. let semaphore = DispatchSemaphore(value: 0)
  353. var result = true
  354. var stateErr = 0
  355. let manager = BiometricStateManager()
  356. manager.hasBiometricStateChanged { (res, state) in
  357. result = res
  358. stateErr = state
  359. semaphore.signal()
  360. }
  361. semaphore.wait()
  362. if result {
  363. if Utils.isMiddleMode() {
  364. DispatchQueue.main.async {
  365. self.submit(fromBiometric: true)
  366. }
  367. }
  368. } else if stateErr == 1 {
  369. DispatchQueue.main.async {
  370. Nexilis.hideLoader {
  371. let errorMessage = "Terjadi Perubahan Biometric (Touch/Face ID)"
  372. let dialog = DialogErrorMFA()
  373. dialog.modalTransitionStyle = .crossDissolve
  374. dialog.modalPresentationStyle = .overCurrentContext
  375. dialog.errorDesc = errorMessage
  376. dialog.method = self.METHOD
  377. dialog.hideTryAgain = (stateErr == 1)
  378. UIApplication.shared.visibleViewController?.present(dialog, animated: true)
  379. }
  380. }
  381. }
  382. }
  383. private func updateUIBasedOnMethod() {
  384. headerTitleLabel.text = METHOD
  385. }
  386. }