[UIKit] Keychain

Junyoung Park·2022년 12월 24일
0

UIKit

목록 보기
125/142
post-thumbnail

Swift: Keychain Introduction (2022) – iOS

Keychain

구현 목표

  • 키체인 구현

구현 태스크

  • 키체인 서비스를 담당할 싱글턴 클래스 구현
  • 등록한 서비스, 계정, 비밀번호를 담당할 UI 및 핸들링 버튼 구현

핵심 코드

func save(service: String, account: String, password: Data) throws {
        // service, account, password, class
        let query: [String: AnyObject] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: service as AnyObject,
            kSecAttrAccount as String: account as AnyObject,
            kSecValueData as String: password as AnyObject
        ]
        let status = SecItemAdd(query as CFDictionary, nil)
        guard status != errSecDuplicateItem else {
            throw KeychainError.duplicateEntry
        }
        guard status == errSecSuccess else {
            throw KeychainError.unknown(status)
        }
        print("Successfully Data Saved")
    }
  • SecItemAdd를 통해 키체인에 데이터 등록
  • kSecValueData를 통해 보관할 데이터를 등록
  • 중복 데이터를 거르거나 특정 에러가 생길 때 핸들링
func get(service: String, account: String) throws -> Data? {
        let query: [String: AnyObject] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: service as AnyObject,
            kSecAttrAccount as String: account as AnyObject,
            kSecReturnData as String: kCFBooleanTrue,
            kSecMatchLimit as String: kSecMatchLimitOne
        ]
        
        var result: AnyObject?
        let status = SecItemCopyMatching(query as CFDictionary, &result)
        print("returned Status: \(status)")
        return result as? Data
    }
  • SecItemCopyMatching을 통해 키체인에 등록되어 있는 데이터와 매칭할 데이터를 비교, AnyObject 옵셔널로 선언된 result 변수의 주솟값을 넘김으로써 쿼리에 해당하는 데이터를 리턴

소스 코드

final class KeychainManager {
    enum KeychainError: LocalizedError {
        case duplicateEntry
        case unknown(OSStatus)
        
        var errorDescription: String? {
            switch self {
            case .duplicateEntry: return "This is Duplicated Entry"
            case .unknown(let status): return "Unknown Error: StatusCode: \(status.description)"
            }
        }
    }
    
    static let shared = KeychainManager()
    private init() {}
    
    func save(service: String, account: String, password: Data) throws {
        // service, account, password, class
        let query: [String: AnyObject] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: service as AnyObject,
            kSecAttrAccount as String: account as AnyObject,
            kSecValueData as String: password as AnyObject
        ]
        let status = SecItemAdd(query as CFDictionary, nil)
        guard status != errSecDuplicateItem else {
            throw KeychainError.duplicateEntry
        }
        guard status == errSecSuccess else {
            throw KeychainError.unknown(status)
        }
        print("Successfully Data Saved")
    }
    
    func get(service: String, account: String) throws -> Data? {
        let query: [String: AnyObject] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: service as AnyObject,
            kSecAttrAccount as String: account as AnyObject,
            kSecReturnData as String: kCFBooleanTrue,
            kSecMatchLimit as String: kSecMatchLimitOne
        ]
        
        var result: AnyObject?
        let status = SecItemCopyMatching(query as CFDictionary, &result)
        print("returned Status: \(status)")
        return result as? Data
    }
}
  • OSStatusCode의 종류를 캐치하기 어렵기 때문에 커스텀 에러를 통해 판별
import SwiftUI
import UIKit

final class KeychainViewController: UIViewController {
    private let services: [String] = ["google.com", "facebook.com", "twitter.com", "apple.com", "naver.com"]
    private let serviceLabel: UILabel = {
        let label = UILabel()
        label.textAlignment = .left
        label.font = .preferredFont(forTextStyle: .headline)
        label.textColor = .systemGray
        label.text = "Pick your Service"
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }()
    private lazy var servicePickerView: UIPickerView = {
        let pickerView = UIPickerView()
        pickerView.translatesAutoresizingMaskIntoConstraints = false
        pickerView.delegate = self
        pickerView.dataSource = self
        return pickerView
    }()
    private lazy var selectedService: String = services[0]
    private let emailTextField: UITextField = {
        let textField = UITextField()
        textField.borderStyle = .roundedRect
        textField.placeholder = "Enter Email"
        textField.keyboardType = .emailAddress
        textField.translatesAutoresizingMaskIntoConstraints = false
        return textField
    }()
    private lazy var passwordTextField: UITextField = {
        let textField = UITextField()
        textField.borderStyle = .roundedRect
        textField.placeholder = "Enter Password"
        textField.textContentType = .oneTimeCode
        textField.translatesAutoresizingMaskIntoConstraints = false
        textField.addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged)
        return textField
    }()
    private lazy var saveButton: UIButton = {
        let button = UIButton()
        var config = UIButton.Configuration.filled()
        config.baseForegroundColor = .white
        config.baseBackgroundColor = .systemGreen
        config.title = "SAVE"
        button.configuration = config
        button.addTarget(self, action: #selector(didTapSave), for: .touchUpInside)
        button.translatesAutoresizingMaskIntoConstraints = false
        return button
    }()
    private lazy var getButton: UIButton = {
        let button = UIButton()
        var config = UIButton.Configuration.filled()
        config.baseForegroundColor = .white
        config.baseBackgroundColor = .systemPink
        config.title = "GET"
        button.configuration = config
        button.addTarget(self, action: #selector(didTapGet), for: .touchUpInside)
        button.translatesAutoresizingMaskIntoConstraints = false
        return button
    }()
    private let resultLabel: UILabel = {
        let label = UILabel()
        label.textAlignment = .center
        label.font = .preferredFont(forTextStyle: .body)
        label.numberOfLines = 0
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }()
    
    private let keychainManager = KeychainManager.shared
    override func viewDidLoad() {
        super.viewDidLoad()
        setUI()
    }
    
    private func setUI() {
        view.backgroundColor = .systemBackground
        title = "Keychain"
        navigationItem.largeTitleDisplayMode = .always
        navigationController?.navigationBar.prefersLargeTitles = true
        view.addSubview(serviceLabel)
        view.addSubview(servicePickerView)
        view.addSubview(emailTextField)
        view.addSubview(passwordTextField)
        view.addSubview(saveButton)
        view.addSubview(getButton)
        view.addSubview(resultLabel)
        applyConstraints()
    }
    
    @objc private func textFieldDidChange(_ textField: UITextField) {
        textField.isSecureTextEntry = true
    }
    
    @objc private func didTapSave() {
        guard
            let email = emailTextField.text,
            !email.isEmpty,
            let password = passwordTextField.text,
            !password.isEmpty else {
            resultLabel.text = "Check your text inputs..."
            return
        }
        save(service: selectedService, account: email, password: password)
    }
    
    @objc private func didTapGet() {
        guard
            let email = emailTextField.text,
            !email.isEmpty else { return }
        get(service: selectedService, account: email)
    }
    
    private func applyConstraints() {
        let serviceLabelConstraints = [
            serviceLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 50),
            serviceLabel.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 10),
            serviceLabel.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -10),
            serviceLabel.heightAnchor.constraint(equalToConstant: 50)
        ]
        NSLayoutConstraint.activate(serviceLabelConstraints)
        
        let servicePickerViewConstraints = [
            servicePickerView.topAnchor.constraint(equalTo: serviceLabel.bottomAnchor, constant: 10),
            servicePickerView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 10),
            servicePickerView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -10),
            servicePickerView.heightAnchor.constraint(equalToConstant: 100)
        ]
        NSLayoutConstraint.activate(servicePickerViewConstraints)
        
        let textFieldHeight: CGFloat = 50
        
        let emailTextFieldConstraints = [
            emailTextField.topAnchor.constraint(equalTo: servicePickerView.bottomAnchor, constant: 10),
            emailTextField.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 10),
            emailTextField.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -10),
            emailTextField.heightAnchor.constraint(equalToConstant: textFieldHeight)
        ]
        NSLayoutConstraint.activate(emailTextFieldConstraints)
        
        let passwordTextFieldConstriants = [
            passwordTextField.topAnchor.constraint(equalTo: emailTextField.bottomAnchor, constant: 10),
            passwordTextField.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 10),
            passwordTextField.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -10),
            passwordTextField.heightAnchor.constraint(equalToConstant: textFieldHeight)
        ]
        NSLayoutConstraint.activate(passwordTextFieldConstriants)
        
        let buttonHeight: CGFloat = 40
        
        let saveButtonConstraints = [
            saveButton.topAnchor.constraint(equalTo: passwordTextField.bottomAnchor, constant: 10),
            saveButton.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 10),
            saveButton.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -10),
            saveButton.heightAnchor.constraint(equalToConstant: buttonHeight)
        ]
        NSLayoutConstraint.activate(saveButtonConstraints)
        
        let getButtonConstraints = [
            getButton.topAnchor.constraint(equalTo: saveButton.bottomAnchor, constant: 10),
            getButton.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 10),
            getButton.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -10),
            getButton.heightAnchor.constraint(equalToConstant: buttonHeight)
        ]
        NSLayoutConstraint.activate(getButtonConstraints)
        
        let resultLabelConstraints = [
            resultLabel.topAnchor.constraint(equalTo: getButton.bottomAnchor, constant: 10),
            resultLabel.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 10),
            resultLabel.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -10),
            resultLabel.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
        ]
        NSLayoutConstraint.activate(resultLabelConstraints)
    }
    
    private func save(service: String, account: String, password: String) {
        guard let passwordData = password.data(using: .utf8) else {
            resultLabel.text = "Password Encoding Failed"
            return
        }
        
        do {
            try keychainManager.save(service: service, account: account, password: passwordData)
            resultLabel.text = "Successfully Saved!"
            emailTextField.text = ""
            passwordTextField.text = ""
        } catch {
            resultLabel.text = error.localizedDescription
        }
    }
    
    private func get(service: String, account: String) {
        do {
            let data = try keychainManager.get(service: service, account: account)
            if let data = data {
                let password = String(decoding: data, as: UTF8.self)
                resultLabel.text = "Returned Password: \(password)"
            } else {
                resultLabel.text = "Returned Data is empty"
            }
        } catch {
            resultLabel.text = error.localizedDescription
        }
    }
}
  • 서비스, 이메일, 패스워드를 입력한 뒤 저장
  • 서비스, 이메일을 통해 패스워드를 리턴
extension KeychainViewController: UIPickerViewDelegate {
    func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
        return services[row]
    }
    
    func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
        selectedService = services[row]
    }
}

extension KeychainViewController: UIPickerViewDataSource {
    func numberOfComponents(in pickerView: UIPickerView) -> Int {
        return 1
    }
    
    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        return services.count
    }
}
  • 서비스 종류를 고르기 위한 픽커를 구성하는 델리게이트 및 데이터 소스 함수

구현 화면

profile
JUST DO IT

0개의 댓글