본문 바로가기
YAGOM CAREER STARTER

[토요스터디A반] 20230408: KeyChain

by Rhode 2023. 4. 14.

본문은 야곰 아카데미 커리어 스타터 캠프를 통해 학습한 내용을 회고한 글입니다.


실험 1: 각 View 역할 이해 및 UserDefault를 활용한 Password의 저장

import UIKit

class LogInViewController: UIViewController {
    
    @IBOutlet weak var pwTextField: UITextField!
    var diaryViewController: DiaryViewController?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        diaryViewController = self.storyboard?.instantiateViewController(withIdentifier: "diary") as? DiaryViewController
    }
    
    @IBAction func tapLogInButton(_ sender: Any) {
        guard let diaryViewController = diaryViewController else { return }
        
        let storedPW = UserDefaults.standard.string(forKey: "passwordKey")
        
        guard pwTextField.text == storedPW else {
            let alert = UIAlertController(title: "비밀번호가 일치하지 않습니다", message: "등록된 비밀번호와 입력한 비밀번호가 일치하지 않습니다", preferredStyle: .alert)
            let confirm = UIAlertAction(title: "돌아가기", style: .default)
            alert.addAction(confirm)
            
            present(alert, animated: true, completion: nil)
            
            return
        }
        diaryViewController.modalPresentationStyle = .fullScreen
        present(diaryViewController, animated: true)
    }
    
    @IBAction func addNewPassword(_ sender: Any) {
        let passwordRegex = "^(?=.*[A-Za-z])(?=.*[0-9])(?=.*[!@#$%^&*()_+=-]).{8,16}"
        let validPw = pwTextField.text?.range(of: passwordRegex, options: .regularExpression) != nil
        
        if validPw {
            guard let newPassword = pwTextField.text, !newPassword.isEmpty else { return }
            
            UserDefaults.standard.set(newPassword, forKey: "passwordKey")
            
            let alert = UIAlertController(title: "성공", message: "비밀번호가 성공적으로 등록되었습니다.", preferredStyle: .alert)
            let confirm = UIAlertAction(title: "확인", style: .default)
            
            alert.addAction(confirm)
            
            present(alert, animated: true, completion: nil)
        } else {
            let alert = UIAlertController(title: "비밀번호 오류", message: "영어 대소문자, 숫자, 특수기호를 넣어 8자리 이상 16자리 이하 비밀번호를 만들어주세요.", preferredStyle: .alert)
            let confirm = UIAlertAction(title: "확인", style: .default)
            
            alert.addAction(confirm)
            
            present(alert, animated: true, completion: nil)
        }
    }
}

실험 2: KeyChain을 활용해 비밀번호를 저장하고, 검증하기

KeyChainManager:

import Foundation

final class KeychainManager {
    static let shared = KeychainManager()
    
    private init() {}
    
    func createKeychain(credentials: Credentials) throws {
        let account = credentials.userName
        let password = credentials.password.data(using: String.Encoding.utf8)!
        let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword,
                                    kSecAttrAccount as String: account,
                                    kSecValueData as String: password]
        let status = SecItemAdd(query as CFDictionary, nil)
        
        guard status != errSecDuplicateItem else {
            throw KeychainError.duplicateItem
        }
        
        guard status == errSecSuccess else {
            throw KeychainError.unhandledError(status: status)
        }
    }
    
    func readKeyChain(account: String) throws -> Credentials {
        let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword,
                                    kSecMatchLimit as String: kSecMatchLimitOne,
                                    kSecAttrAccount as String: account,
                                    kSecReturnAttributes as String: true,
                                    kSecReturnData as String: true]
        var item: CFTypeRef?
        let status = SecItemCopyMatching(query as CFDictionary, &item)
        
        guard status != errSecItemNotFound else {
            throw KeychainError.noPassword
        }
        
        guard status == errSecSuccess else {
            throw KeychainError.unhandledError(status: status)
        }
        
        guard let existingItem = item as? [String: Any],
              let passwordData = existingItem[kSecValueData as String] as? Data,
              let password = String(data: passwordData, encoding: String.Encoding.utf8),
              let account = existingItem[kSecAttrAccount as String] as? String else {
            throw KeychainError.unexpectedPasswordData
        }
        
        let credentials = Credentials(userName: account, password: password)
        
        return credentials
    }
    
    func updateKeyChain(credentials: Credentials) throws {
        let account = credentials.userName
        let password = credentials.password.data(using: String.Encoding.utf8)!
        let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword,
                                    kSecAttrAccount as String: account]
        let attributes: [String: Any] = [kSecValueData as String: password]
        let status = SecItemUpdate(query as CFDictionary, attributes as CFDictionary)
        
        guard status != errSecDuplicateItem else {
            throw KeychainError.duplicateItem
        }
        
        guard status != errSecItemNotFound else {
            throw KeychainError.noPassword
        }
        
        guard status == errSecSuccess else {
            throw KeychainError.unhandledError(status: status)
        }
    }
    
}

LogInViewController:

import UIKit

class LogInViewController: UIViewController {
    let account = "Rhode"
    let keyChainManager = KeychainManager.shared
    let alertManager = AlertManager.shared
    
    @IBOutlet weak var pwTextField: UITextField!
    var diaryViewController: DiaryViewController?
    var changePasswordViewController: ChangePasswordViewController?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        diaryViewController = self.storyboard?.instantiateViewController(withIdentifier: "diary") as? DiaryViewController
        changePasswordViewController = self.storyboard?.instantiateViewController(identifier: "newPW") as? ChangePasswordViewController
    }
    
    @IBAction func tapLogInButton(_ sender: Any) {
        do {
            let loaded = try keyChainManager.readKeyChain(account: account)
            guard loaded.password == pwTextField.text else {
                alertManager.presentAlert(viewController: self, title: "비밀번호 불일치", message: "등록된 비밀번호와 입력한 비밀번호가 일치하지 않습니다.")
                
                return
            }
        } catch {
            alertManager.presentAlert(viewController: self, title: "에러", message: "\(error)")
        }
        
        guard let diaryViewController = diaryViewController else { return }
        diaryViewController.modalPresentationStyle = .fullScreen
        present(diaryViewController, animated: true)
    }
    
    @IBAction func addNewPassword(_ sender: Any) {
        let passwordRegex = "^(?=.*[A-Za-z])(?=.*[0-9])(?=.*[!@#$%^&*()_+=-]).{8,16}"
        let validPw = pwTextField.text?.range(of: passwordRegex, options: .regularExpression) != nil
        
        if validPw {
            guard let newPassword = pwTextField.text, !newPassword.isEmpty else {
                alertManager.presentAlert(viewController: self, title: "비밀번호 입력 요망", message: "비밀번호를 입력해주세요.")
                
                return
            }
            
            let credentials = Credentials(userName: account, password: newPassword)
            
            do {
                try keyChainManager.createKeychain(credentials: credentials)
                alertManager.presentAlert(viewController: self, title: "성공", message: "비밀번호가 성공적으로 등록되었습니다.")
            } catch KeychainError.duplicateItem {
                alertManager.presentAlert(viewController: self, title: "중복된 계정", message: "이미 계정에 비밀번호가 있습니다.")
            } catch {
                alertManager.presentAlert(viewController: self, title: "에러", message: "\(error)")
            }
        } else {
            alertManager.presentAlert(viewController: self, title: "비밀번호 오류", message: "영어 대소문자, 숫자, 특수기호를 넣어 8자리 이상 16자리 이하 비밀번호를 만들어주세요.")
            
            return
        }
    }
    
    @IBAction func tapChangePasswordButton(_ sender: UIButton) {
        guard let changePasswordViewController = changePasswordViewController else { return }
        changePasswordViewController.modalPresentationStyle = .popover
        present(changePasswordViewController, animated: true)
    }
}

실험 3: 비밀번호를 대조하여 새로운 비밀번호로 업데이트 하기

ChangePasswordViewController:

import UIKit

class ChangePasswordViewController: UIViewController {
    let account = "Rhode"
    let keyChainManager = KeychainManager.shared
    let alertManager = AlertManager.shared
    
    @IBOutlet weak var currentPWTextField: UITextField!
    @IBOutlet weak var newPWTextField: UITextField!
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    @IBAction func tapChangePasswordButton(_ sender: Any) {
        do {
            let loaded = try keyChainManager.readKeyChain(account: account)
            guard loaded.password == currentPWTextField.text else {
                alertManager.presentAlert(viewController: self, title: "비밀번호 불일치", message: "등록된 비밀번호와 입력한 비밀번호가 일치하지 않습니다.")
                
                return
            }
        } catch {
            alertManager.presentAlert(viewController: self, title: "에러", message: "\(error)")
        }
        
        do {
            let passwordRegex = "^(?=.*[A-Za-z])(?=.*[0-9])(?=.*[!@#$%^&*()_+=-]).{8,16}"
            let validPw = newPWTextField.text?.range(of: passwordRegex, options: .regularExpression) != nil
            
            if validPw {
                guard let newPassword = newPWTextField.text, !newPassword.isEmpty else {
                    alertManager.presentAlert(viewController: self, title: "비밀번호 입력 요망", message: "비밀번호를 입력해주세요.")
                    
                    return
                }
                
                let credentials = Credentials(userName: account, password: newPassword)
                
                try keyChainManager.updateKeyChain(credentials: credentials)
                
                alertManager.presentAlert(viewController: self, title: "비밀번호 변경 성공", message: "비밀번호가 변경되었습니다.") {
                    self.dismiss(animated: true)
                }
            } else {
                alertManager.presentAlert(viewController: self, title: "비밀번호 오류", message: "영어 대소문자, 숫자, 특수기호를 넣어 8자리 이상 16자리 이하 비밀번호를 만들어주세요.")
            }
        } catch {
            alertManager.presentAlert(viewController: self, title: "에러", message: "\(error)")
        }
    }
    
}