kevin 2 недель назад
Родитель
Сommit
d8af84ffd1

+ 21 - 0
NexilisLite/NexilisLite/Resource/Assets.xcassets/pb_mfa_bjb.imageset/Contents.json

@@ -0,0 +1,21 @@
+{
+  "images" : [
+    {
+      "filename" : "pb_mfa_bjb.png",
+      "idiom" : "universal",
+      "scale" : "1x"
+    },
+    {
+      "idiom" : "universal",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "universal",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

BIN
NexilisLite/NexilisLite/Resource/Assets.xcassets/pb_mfa_bjb.imageset/pb_mfa_bjb.png


+ 21 - 0
NexilisLite/NexilisLite/Resource/Assets.xcassets/pb_mfa_splash.imageset/Contents.json

@@ -0,0 +1,21 @@
+{
+  "images" : [
+    {
+      "filename" : "pb_mfa_splash.png",
+      "idiom" : "universal",
+      "scale" : "1x"
+    },
+    {
+      "idiom" : "universal",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "universal",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

BIN
NexilisLite/NexilisLite/Resource/Assets.xcassets/pb_mfa_splash.imageset/pb_mfa_splash.png


+ 44 - 0
NexilisLite/NexilisLite/Source/APIS.swift

@@ -511,6 +511,47 @@ public class APIS: NSObject {
         }
     }
     
+    public static func openMFA(method: String, flag: Int){
+        let isChangeProfile = Utils.getSetProfile()
+        if !isChangeProfile {
+            APIS.showChangeProfile()
+            return
+        }
+        if flag == MFAViewController.STEP_NEEDED_FIDO {
+            if let me = User.getMyPin() {
+                if let response = Nexilis.writeAndWait(message: CoreMessage_TMessageBank.getMFAValidation(data: me)) {
+                    if response.isOk() {
+                        UIApplication.shared.visibleViewController?.view.makeToast("Action Successful".localized(), duration: 3)
+                    }
+                    else {
+                        UIApplication.shared.visibleViewController?.view.makeToast(response.mBodies[CoreMessage_TMessageKey.MESSAGE_TEXT], duration: 3)
+                    }
+                }
+            }
+            
+        }
+        else if flag == MFAViewController.STEP_NEEDED_FINGER {
+            let controller = MFAOnlyBiometricViewController()
+            let navigationController = CustomNavigationController(rootViewController: controller)
+            navigationController.defaultStyle()
+            if UIApplication.shared.visibleViewController?.navigationController != nil {
+                UIApplication.shared.visibleViewController?.navigationController?.present(navigationController, animated: true, completion: nil)
+            } else {
+                UIApplication.shared.visibleViewController?.present(navigationController, animated: true, completion: nil)
+            }
+        }
+        else {
+            let controller = MFAViewController()
+            let navigationController = CustomNavigationController(rootViewController: controller)
+            navigationController.defaultStyle()
+            if UIApplication.shared.visibleViewController?.navigationController != nil {
+                UIApplication.shared.visibleViewController?.navigationController?.present(navigationController, animated: true, completion: nil)
+            } else {
+                UIApplication.shared.visibleViewController?.present(navigationController, animated: true, completion: nil)
+            }
+        }
+    }
+    
     public static func openSecureBrowser() {
         let isChangeProfile = Utils.getSetProfile()
         if !isChangeProfile {
@@ -2414,6 +2455,9 @@ public class APIS: NSObject {
     public static func getnameGroupShared() -> String {
         return nameGroupShared
     }
+    
+    
+    
 }
 
 extension UINavigationController {

+ 16 - 0
NexilisLite/NexilisLite/Source/CoreMessage_TMessageBank.swift

@@ -2842,4 +2842,20 @@ public class CoreMessage_TMessageBank {
         return tMessage
     }
     
+    public static func getMFAValidation(data: String) -> TMessage {
+        let tMessage = NexilisLite.TMessage()
+        tMessage.mPIN = data
+        tMessage.mCode = CoreMessage_TMessageCode.DEMO_FIDO
+        tMessage.mStatus = CoreMessage_TMessageUtil.getTID()
+        return tMessage
+    }
+    
+    public static func getAuthRequest(data: String) -> TMessage {
+        let tMessage = NexilisLite.TMessage()
+        tMessage.mPIN = data
+        tMessage.mCode = CoreMessage_TMessageCode.AUTH_REQUEST
+        tMessage.mStatus = CoreMessage_TMessageUtil.getTID()
+        return tMessage
+    }
+    
 }

+ 3 - 0
NexilisLite/NexilisLite/Source/CoreMessage_TMessageCode.swift

@@ -828,5 +828,8 @@ public class CoreMessage_TMessageCode {
     public static let CHANGE_COMMUNITY_INFO = "CM11";
     public static let REPORT_COMMUNITY = "CM12";
     public static let CHANGE_COMMUNITY_POSITION = "CM13";
+    
+    public static let GET_MULTI_FACTOR_AUTHENTICATION = "GMFA";
+    public static let DEMO_FIDO = "DF01";
 
 }

+ 2 - 0
NexilisLite/NexilisLite/Source/CoreMessage_TMessageKey.swift

@@ -514,4 +514,6 @@ public class CoreMessage_TMessageKey {
     public static let ACT_VALUE = "ACTV";
     public static let ACT_TYPE = "ACTT";
     public static let ACT_TIME = "ACTMS";
+    
+    public static let TOTP = "TOTP";
 }

+ 4 - 0
NexilisLite/NexilisLite/Source/Nexilis.swift

@@ -2552,6 +2552,10 @@ public class Nexilis: NSObject {
     
     weak open var connectionDelegate: ConnectionDelegate?
     
+    weak open var mfaDelegate: MFADelegate?
+    
+    weak open var authDelegate: AuthenticationDelegate?
+    
     var floating: FloatingNotificationBanner!
     
     var stateUnfriend = ""

+ 63 - 0
NexilisLite/NexilisLite/Source/View/Control/MFABiometricOnlyViewController.swift

@@ -0,0 +1,63 @@
+//
+//  MFABiometricOnlyViewController.swift
+//  Pods
+//
+//  Created by Maronakins on 01/08/25.
+//
+
+import UIKit
+import LocalAuthentication
+
+class MFAOnlyBiometricViewController: UIViewController {
+
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        self.view.backgroundColor = .clear // Equivalent to setBackgroundColor(Color.TRANSPARENT)
+        dummyNextStep()
+    }
+
+    private func dummyNextStep() {
+        // This is where you would call the biometric authentication
+        biometricAuth(onSuccess: {
+            Nexilis.shared.authDelegate?.onAuthenticationSucceeded()
+            self.dismiss(animated: true, completion: nil) // Equivalent to finish()
+        }, onFailed: { error in
+            Nexilis.shared.authDelegate?.onAuthenticationFailed(error: error)
+            // Handle the error here, e.g., show a dialog
+        })
+    }
+    
+    private func biometricAuth(onSuccess: @escaping () -> Void, onFailed: @escaping (Error?) -> Void) {
+        let context = LAContext()
+        var error: NSError?
+
+        // 1. Check if the device can evaluate the biometric policy.
+        // This is equivalent to BiometricManager.canAuthenticate in Java.
+        if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) {
+            let reason = "Confirm your identity" // This is the title of the prompt
+
+            // 2. Present the biometric authentication prompt.
+            context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { success, authenticationError in
+                DispatchQueue.main.async {
+                    if success {
+                        // Biometric authentication succeeded.
+                        onSuccess()
+                    } else {
+                        // Biometric authentication failed or was cancelled.
+                        onFailed(authenticationError)
+                    }
+                }
+            }
+        } else {
+            // Biometrics are not available or an error occurred.
+            // This handles cases like a user not having Touch ID/Face ID enabled.
+            onFailed(error)
+        }
+    }
+}
+
+// Protocol to handle the authentication result
+public protocol AuthenticationDelegate: AnyObject {
+    func onAuthenticationSucceeded()
+    func onAuthenticationFailed(error: Error?)
+}

+ 466 - 0
NexilisLite/NexilisLite/Source/View/Control/MFAViewController.swift

@@ -0,0 +1,466 @@
+//
+//  MFAViewController.swift
+//  Pods
+//
+//  Created by Maronakins on 01/08/25.
+//
+
+import UIKit
+import Foundation
+import os
+import LocalAuthentication
+
+// MARK: - MFA Class
+class MFAViewController: UIViewController {
+
+    // MARK: - Constants & Variables
+    private let TAG = "MFA"
+
+    // Step definitions mirroring the Java code
+    static let STEP_NEEDED_FIDO = 1
+    static let STEP_NEEDED_FIDO_PWD = 2
+    static let STEP_NEEDED_FIDO_PWD_FINGER = 3
+    static let STEP_NEEDED_FINGER = 4
+    
+    // Properties to be set on initialization
+    var STEP_NEEDED = STEP_NEEDED_FIDO_PWD
+    var METHOD = ""
+    
+    private var mfaState = "open"
+    private var challengeString = ""
+
+    // MARK: - UI Components
+    private let imageViewBackground = UIImageView()
+    private let scrollView = UIScrollView()
+    private let mainStackView = UIStackView()
+    private let headerImageView1 = UIImageView()
+    private let headerImageView2 = UIImageView()
+    private let headerTitleLabel = UILabel()
+    private let subtitleLabel = UILabel()
+    private let passwordTextField = UITextField()
+    private let passwordVisibilityButton = UIButton(type: .system)
+    private let submitButton = UIButton(type: .system)
+    private let poweredStackView = UIStackView()
+    private let poweredLabel = UILabel()
+    private let poweredImageView = UIImageView()
+
+    private var isPasswordVisible = false
+
+    // MARK: - Lifecycle Methods
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        setupUI()
+        setupLayout()
+        loadData()
+        updateUIBasedOnMethod()
+        updateUIBasedOnMode()
+    }
+
+    override func viewWillDisappear(_ animated: Bool) {
+        super.viewWillDisappear(animated)
+        // Notify delegate if the view is closed prematurely
+        if mfaState == "open" {
+            Nexilis.shared.mfaDelegate?.onFailed(error: "back")
+        }
+    }
+
+    // MARK: - UI Setup
+    private func setupUI() {
+        view.backgroundColor = .systemBackground
+
+        // Background Image View
+        imageViewBackground.contentMode = .scaleAspectFill
+        imageViewBackground.translatesAutoresizingMaskIntoConstraints = false
+        view.addSubview(imageViewBackground)
+
+        // Scroll View
+        scrollView.showsVerticalScrollIndicator = false
+        scrollView.translatesAutoresizingMaskIntoConstraints = false
+        view.addSubview(scrollView)
+
+        // Main Stack View
+        mainStackView.axis = .vertical
+        mainStackView.alignment = .center
+        mainStackView.spacing = 16
+        mainStackView.translatesAutoresizingMaskIntoConstraints = false
+        scrollView.addSubview(mainStackView)
+
+        // Header Images
+        headerImageView1.contentMode = .scaleAspectFit
+        headerImageView1.image = UIImage(named: "pb_mfa_bjb")
+        headerImageView1.heightAnchor.constraint(equalToConstant: 100).isActive = true
+        mainStackView.addArrangedSubview(headerImageView1)
+
+        headerImageView2.contentMode = .scaleAspectFit
+        headerImageView2.image = UIImage(named: "pb_mfa_splash")
+        headerImageView2.heightAnchor.constraint(equalToConstant: 200).isActive = true
+        mainStackView.addArrangedSubview(headerImageView2)
+
+        // Header Title Label
+        headerTitleLabel.font = UIFont(name: "Poppins-Bold", size: 17)
+        headerTitleLabel.textAlignment = .center
+        headerTitleLabel.numberOfLines = 0
+        mainStackView.addArrangedSubview(headerTitleLabel)
+
+        // Subtitle Label
+        subtitleLabel.text = "Please input your password to continue"
+        subtitleLabel.font = UIFont(name: "Poppins-Regular", size: 12)
+        subtitleLabel.textAlignment = .center
+        subtitleLabel.numberOfLines = 0
+        mainStackView.addArrangedSubview(subtitleLabel)
+        
+        // Password Input Container
+        let passwordContainerView = UIView()
+        passwordContainerView.translatesAutoresizingMaskIntoConstraints = false
+        passwordContainerView.widthAnchor.constraint(equalToConstant: 300).isActive = true
+        passwordContainerView.heightAnchor.constraint(equalToConstant: 48).isActive = true
+        mainStackView.addArrangedSubview(passwordContainerView)
+        
+        // Password Text Field
+        passwordTextField.placeholder = "Type your password..."
+        passwordTextField.isSecureTextEntry = true
+        passwordTextField.font = UIFont(name: "Poppins-Regular", size: 15)
+        passwordTextField.borderStyle = .roundedRect
+        passwordTextField.keyboardType = .default
+        passwordTextField.autocapitalizationType = .none
+        passwordTextField.autocorrectionType = .no
+        passwordTextField.translatesAutoresizingMaskIntoConstraints = false
+        passwordContainerView.addSubview(passwordTextField)
+        
+        // Password Visibility Button
+        passwordVisibilityButton.setImage(UIImage(systemName: "eye.slash.fill"), for: .normal)
+        passwordVisibilityButton.addTarget(self, action: #selector(togglePasswordVisibility), for: .touchUpInside)
+        passwordVisibilityButton.translatesAutoresizingMaskIntoConstraints = false
+        passwordContainerView.addSubview(passwordVisibilityButton)
+        
+        // Submit Button
+        submitButton.setTitle("SUBMIT", for: .normal)
+        submitButton.backgroundColor = UIColor(hex: "#14507f")
+        submitButton.setTitleColor(.white, for: .normal)
+        submitButton.layer.cornerRadius = 24
+        submitButton.titleLabel?.font = UIFont(name: "Poppins-Regular", size: 16)
+        submitButton.addTarget(self, action: #selector(submitAction), for: .touchUpInside)
+        submitButton.heightAnchor.constraint(equalToConstant: 48).isActive = true
+        submitButton.widthAnchor.constraint(equalToConstant: 300).isActive = true
+        mainStackView.addArrangedSubview(submitButton)
+        
+        mainStackView.setCustomSpacing(12, after: subtitleLabel)
+        mainStackView.setCustomSpacing(24, after: passwordContainerView)
+
+        // Powered By StackView
+        poweredStackView.axis = .horizontal
+        poweredStackView.alignment = .center
+        poweredStackView.spacing = 8
+        poweredStackView.translatesAutoresizingMaskIntoConstraints = false
+        view.addSubview(poweredStackView)
+
+        poweredLabel.text = "Powered by"
+        poweredLabel.font = .systemFont(ofSize: 12)
+        poweredStackView.addArrangedSubview(poweredLabel)
+
+        poweredImageView.contentMode = .scaleAspectFit
+        poweredImageView.image = UIImage(named: "pb_powered_button")
+        poweredImageView.widthAnchor.constraint(equalToConstant: 25).isActive = true
+        poweredImageView.heightAnchor.constraint(equalToConstant: 25).isActive = true
+        poweredStackView.addArrangedSubview(poweredImageView)
+    }
+
+    private func setupLayout() {
+        NSLayoutConstraint.activate([
+            // Background
+            imageViewBackground.topAnchor.constraint(equalTo: view.topAnchor),
+            imageViewBackground.leadingAnchor.constraint(equalTo: view.leadingAnchor),
+            imageViewBackground.trailingAnchor.constraint(equalTo: view.trailingAnchor),
+            imageViewBackground.bottomAnchor.constraint(equalTo: view.bottomAnchor),
+
+            // Scroll View
+            scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
+            scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
+            scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
+            scrollView.bottomAnchor.constraint(equalTo: poweredStackView.topAnchor, constant: -8),
+
+            // Main Stack View
+            mainStackView.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 20),
+            mainStackView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
+            mainStackView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
+            mainStackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
+            mainStackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
+            
+            // Password Field
+            passwordTextField.leadingAnchor.constraint(equalTo: passwordTextField.superview!.leadingAnchor),
+            passwordTextField.trailingAnchor.constraint(equalTo: passwordTextField.superview!.trailingAnchor),
+            passwordTextField.topAnchor.constraint(equalTo: passwordTextField.superview!.topAnchor),
+            passwordTextField.bottomAnchor.constraint(equalTo: passwordTextField.superview!.bottomAnchor),
+            
+            // Password Visibility Button
+            passwordVisibilityButton.trailingAnchor.constraint(equalTo: passwordTextField.trailingAnchor, constant: -8),
+            passwordVisibilityButton.centerYAnchor.constraint(equalTo: passwordTextField.centerYAnchor),
+            passwordVisibilityButton.widthAnchor.constraint(equalToConstant: 40),
+            passwordVisibilityButton.heightAnchor.constraint(equalToConstant: 40),
+
+            // Powered by
+            poweredStackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -16),
+            poweredStackView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -8),
+        ])
+    }
+    
+    // MARK: - Data & Logic
+    private func loadData() {
+        // Fetch and apply "Powered By" text from shared configuration
+        let poweredText = "Nexilis"
+        if !poweredText.isEmpty {
+            poweredLabel.text = "Powered by \(poweredText)"
+        }
+        
+        setBackground()
+    }
+    
+    private func randomizeBackground(from list: String) {
+        
+    }
+
+    private func setBackground() {
+
+    }
+    
+    // MARK: - Actions
+    @objc private func togglePasswordVisibility() {
+        isPasswordVisible.toggle()
+        passwordTextField.isSecureTextEntry = !isPasswordVisible
+        let iconName = isPasswordVisible ? "eye.fill" : "eye.slash.fill"
+        passwordVisibilityButton.setImage(UIImage(systemName: iconName), for: .normal)
+    }
+
+    @objc private func submitAction() {
+        // Check for network connectivity
+//        if Nexilis.shared.getNetworkState() != 1 {
+//            self.showToast(message: "Connection failed. Please check your network.")
+//            return
+//        }
+//        
+        guard let password = passwordTextField.text, !password.trimmingCharacters(in: .whitespaces).isEmpty else {
+            self.showToast(message: "Password cannot be empty.")
+            return
+        }
+
+        guard password.count >= 6 else {
+            self.showToast(message: "Password must be at least 6 characters.")
+            return
+        }
+
+        if STEP_NEEDED == MFAViewController.STEP_NEEDED_FIDO_PWD_FINGER {
+            biometricAuth()
+        } else {
+            submit()
+        }
+    }
+
+    private func submit() {
+        guard let password = passwordTextField.text else { return }
+        self.showToast(message: "Please wait...", duration: 2.0)
+        
+        DispatchQueue.global().async {
+            // 1. Encrypt password
+            let encryptedPwd = password
+            
+            // 2. Create message for the server
+            var tMessage = TMessage()
+            tMessage.mBodies[CoreMessage_TMessageKey.PSWD] = encryptedPwd
+            tMessage.mBodies[CoreMessage_TMessageKey.ACTVITY] =  self.METHOD
+            
+            // 3. Add FIDO and TOTP data to the message
+            self.getFIDO(tMessage: &tMessage)
+            do {
+                let totp = try TOTPGenerator.generate(base32Secret: "JBSWY3DPEHPK3PXP", digits: 6, timeStepSeconds: 30)
+                tMessage.mBodies[CoreMessage_TMessageKey.TOTP] = totp
+            } catch {
+                os_log("Failed to generate TOTP: %{public}@", log: .default, type: .error, error.localizedDescription)
+            }
+
+            // 4. Send the message and wait for a response
+            if let response = Nexilis.writeAndWait(message: tMessage) {
+                DispatchQueue.main.async {
+//                    guard let response = response else {
+//                        self.showToast(message: "Connection failed.")
+//                        Nexilis.shared.mfaDelegate?.onFailed(error: "Connection failed.")
+//                        self.dismiss(animated: true)
+//                        return
+//                    }
+                    
+                    if response.isOk() {
+                        self.mfaState = "success"
+                        self.showToast(message: "Success!")
+                        Nexilis.shared.mfaDelegate?.onSuccess()
+                        self.dismiss(animated: true)
+                    } else {
+                        self.mfaState = "failed"
+                        let errorMessage = response.mBodies[CoreMessage_TMessageKey.MESSAGE_TEXT] ?? "An unknown error occurred."
+                        self.showToast(message: errorMessage)
+                        Nexilis.shared.mfaDelegate?.onFailed(error: errorMessage)
+                        self.dismiss(animated: true)
+                    }
+                }
+            }
+        }
+    }
+    
+    private func getFIDO(tMessage: inout TMessage) {
+//        do {
+//            let fingerprint = try HMACDeviceFingerprint.generate()
+//            tMessage.putBody(key: CoreMessage_TMessageKey.FINGERPRINT, value: fingerprint)
+//            
+//            if KeyManagerNexilis.hasGeneratedKey() {
+//                // If key exists, sign the challenge and fingerprint
+//                let signature = try KeyManagerNexilis.sign(data: self.challengeString + "!" + fingerprint)
+//                tMessage.putBody(key: CoreMessage_TMessageKey.SIGNATURE, value: signature)
+//            } else {
+//                // Otherwise, generate a new key and send the public part
+//                try KeyManagerNexilis.generateKey()
+//                let publicKey = try KeyManagerNexilis.getPublicKeyEncoded()
+//                tMessage.putBody(key: CoreMessage_TMessageKey.PUBLIC_KEY, value: publicKey)
+//            }
+//        } catch {
+//            os_log("FIDO operation failed: %{public}@", log: .default, type: .error, error.localizedDescription)
+//        }
+    }
+
+    private func biometricAuth() {
+        let context = LAContext()
+        var error: NSError?
+
+        if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) {
+            let reason = "Confirm your identity to proceed"
+            context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { [weak self] success, authenticationError in
+                DispatchQueue.main.async {
+                    guard let self = self else { return }
+                    if success {
+                        os_log("Biometric authentication succeeded.", log: .default, type: .info)
+                        self.showToast(message: "Please wait...", duration: 2.0)
+                        if KeyManagerNexilis.hasGeneratedKey() {
+                            self.requestAuth() // FIDO flow
+                        } else {
+                            self.submit() // Standard password submission
+                        }
+                    } else {
+                        os_log("Biometric authentication failed.", log: .default, type: .error)
+                        let errorMessage = authenticationError?.localizedDescription ?? "Authentication failed."
+                        self.showToast(message: errorMessage)
+                    }
+                }
+            }
+        } else {
+            os_log("Biometrics not available. Falling back to standard submission.", log: .default, type: .info)
+            // If biometrics are not available on the device, proceed with the standard submission.
+            self.submit()
+        }
+    }
+    
+    private func requestAuth() {
+        DispatchQueue.global().async {
+            // Request a challenge from the server
+            var appname = APIS.getAppNm()
+            if let response = Nexilis.writeAndWait(message: CoreMessage_TMessageBank.getAuthRequest(data: appname)) {
+                
+                if !response.isOk()  {
+                    self.showToast(message: "Action failed: Could not get challenge.")
+                    return
+                }
+                
+                // On success, store the challenge and then proceed with the final submission
+                self.challengeString = response.getBody(key: CoreMessage_TMessageKey.DATA) ?? ""
+                self.submit()
+            }
+        }
+    }
+
+    // MARK: - UI Updates
+    override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
+        super.traitCollectionDidChange(previousTraitCollection)
+        if traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) {
+            updateUIBasedOnMode()
+            setBackground()
+        }
+    }
+    
+    private func updateUIBasedOnMode() {
+        let isDarkMode = traitCollection.userInterfaceStyle == .dark
+        let textColor: UIColor = isDarkMode ? .white : .black
+        let placeholderColor: UIColor = .gray
+        
+        passwordTextField.textColor = textColor
+        passwordTextField.attributedPlaceholder = NSAttributedString(
+            string: "Type your password...",
+            attributes: [.foregroundColor: placeholderColor]
+        )
+        passwordVisibilityButton.tintColor = textColor
+        headerTitleLabel.textColor = textColor
+        subtitleLabel.textColor = isDarkMode ? .lightGray : .darkGray
+        poweredLabel.textColor = isDarkMode ? .lightGray : .darkGray
+        submitButton.backgroundColor = isDarkMode ? UIColor(hex: "#2E77AE") : UIColor(hex: "#14507f")
+    }
+    
+    private func updateUIBasedOnMethod() {
+        if METHOD.contains("Sign Up") {
+            headerTitleLabel.text = "Register"
+        } else {
+            headerTitleLabel.text = "Authenticate Your Account"
+        }
+    }
+}
+
+// MARK: - Helper Extensions & Mocks
+
+// Helper for Hex Color
+extension UIColor {
+    convenience init(hex: String) {
+        var cString: String = hex.trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
+        if cString.hasPrefix("#") { cString.removeFirst() }
+        guard cString.count == 6 else { self.init(white: 0.6, alpha: 1.0); return }
+        
+        var rgbValue: UInt64 = 0
+        Scanner(string: cString).scanHexInt64(&rgbValue)
+        
+        self.init(red: CGFloat((rgbValue & 0xFF0000) >> 16) / 255.0,
+                  green: CGFloat((rgbValue & 0x00FF00) >> 8) / 255.0,
+                  blue: CGFloat(rgbValue & 0x0000FF) / 255.0,
+                  alpha: 1.0)
+    }
+}
+
+// Helper to show Android-style Toast messages
+extension UIViewController {
+    func showToast(message: String, duration: Double = 3.0) {
+        let toastLabel = UILabel()
+        toastLabel.backgroundColor = UIColor.black.withAlphaComponent(0.6)
+        toastLabel.textColor = .white
+        toastLabel.font = .systemFont(ofSize: 14.0)
+        toastLabel.textAlignment = .center
+        toastLabel.text = message
+        toastLabel.alpha = 1.0
+        toastLabel.layer.cornerRadius = 10
+        toastLabel.clipsToBounds = true
+        
+        self.view.addSubview(toastLabel)
+        
+        toastLabel.translatesAutoresizingMaskIntoConstraints = false
+        NSLayoutConstraint.activate([
+            toastLabel.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
+            toastLabel.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor, constant: -50),
+            toastLabel.widthAnchor.constraint(lessThanOrEqualTo: self.view.widthAnchor, constant: -40),
+            toastLabel.heightAnchor.constraint(equalToConstant: 40)
+        ])
+        
+        UIView.animate(withDuration: 0.5, delay: duration, options: .curveEaseOut, animations: {
+            toastLabel.alpha = 0.0
+        }, completion: { _ in
+            toastLabel.removeFromSuperview()
+        })
+    }
+}
+
+// Delegate protocol for callbacks
+public protocol MFADelegate: AnyObject {
+    func onSuccess()
+    func onFailed(error: String)
+}
+

+ 103 - 0
NexilisLite/NexilisLite/Source/View/Control/TOTPGenerator.swift

@@ -0,0 +1,103 @@
+//
+//  TOTPGenerator.swift
+//  Pods
+//
+//  Created by Maronakins on 01/08/25.
+//
+
+
+import Foundation
+import CryptoKit
+
+/// A utility for generating Time-based One-Time Passwords (TOTP).
+public struct TOTPGenerator {
+
+    /// Generates a Time-based One-Time Password (TOTP) using the given parameters.
+    ///
+    /// This implementation follows the standards defined in RFC 4226 (HOTP) and RFC 6238 (TOTP).
+    ///
+    /// - Parameters:
+    ///   - base32Secret: The shared secret key, encoded in Base32 string format.
+    ///   - digits: The number of digits the OTP should have (typically 6 or 8).
+    ///   - timeStepSeconds: The time interval in seconds for which an OTP is valid (typically 30 or 60).
+    /// - Returns: A formatted OTP string with the specified number of digits, or `nil` if the Base32 secret is invalid.
+    public static func generate(base32Secret: String, digits: Int, timeStepSeconds: Int) -> String? {
+        // 1. Decode the Base32 secret into raw bytes.
+        guard let secretData = base32Decode(base32Secret) else {
+            print("Error: Invalid Base32 secret string.")
+            return nil
+        }
+
+        // 2. Calculate the current time step (counter).
+        // This is the number of `timeStepSeconds` intervals that have passed since the Unix epoch.
+        let timeStep = UInt64(Date().timeIntervalSince1970) / UInt64(timeStepSeconds)
+
+        // 3. Convert the time step to an 8-byte, big-endian data representation.
+        let timeStepData = withUnsafeBytes(of: timeStep.bigEndian) { Data($0) }
+        
+        // 4. Compute the HMAC-SHA1 hash.
+        let key = SymmetricKey(data: secretData)
+        let hmac = HMAC<Insecure.SHA1>.authenticationCode(for: timeStepData, using: key)
+
+        // 5. Perform dynamic truncation to get a 4-byte value.
+        // Convert HMAC to an array of bytes to work with.
+        let hashBytes = Array(hmac)
+        
+        // The last 4 bits of the hash determine the offset.
+        let offset = Int(hashBytes.last! & 0x0F)
+        
+        // Extract 4 bytes from the hash at the calculated offset.
+        let truncatedHash =
+            (UInt32(hashBytes[offset]     & 0x7F) << 24) |
+            (UInt32(hashBytes[offset + 1] & 0xFF) << 16) |
+            (UInt32(hashBytes[offset + 2] & 0xFF) << 8)  |
+            (UInt32(hashBytes[offset + 3] & 0xFF))
+
+        // 6. Generate the final OTP value.
+        // Calculate the divisor (10^digits).
+        let divisor = NSDecimalNumber(decimal: pow(10, digits)).uint32Value
+        
+        // The OTP is the remainder of the division.
+        let otp = truncatedHash % divisor
+
+        // 7. Format the OTP string, padding with leading zeros if necessary.
+        return String(format: "%0\(digits)d", otp)
+    }
+
+    /// Decodes a Base32 encoded string into raw data bytes.
+    ///
+    /// This minimal decoder adheres to RFC 4648 and ignores padding characters.
+    ///
+    /// - Parameter base32: The Base32 encoded string.
+    /// - Returns: A `Data` object containing the decoded bytes, or `nil` if the input is malformed.
+    private static func base32Decode(_ base32: String) -> Data? {
+        let base32Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"
+        let base32CharsMap = Dictionary(uniqueKeysWithValues: base32Chars.enumerated().map { ($1, $0) })
+        
+        // Sanitize the input: convert to uppercase and remove any padding.
+        let sanitizedBase32 = base32.uppercased().replacingOccurrences(of: "=", with: "")
+        
+        var data = Data()
+        var buffer = 0
+        var bitsLeft = 0
+        
+        for char in sanitizedBase32 {
+            guard let value = base32CharsMap[char] else {
+                // Invalid character found in the Base32 string.
+                return nil
+            }
+            
+            buffer <<= 5
+            buffer |= value
+            bitsLeft += 5
+            
+            if bitsLeft >= 8 {
+                let byte = (buffer >> (bitsLeft - 8)) & 0xFF
+                data.append(UInt8(byte))
+                bitsLeft -= 8
+            }
+        }
+        
+        return data
+    }
+}