Skip to content

[week14] KeyChain

Hemg edited this page Aug 12, 2023 · 3 revisions

KeyChain Service

UserDefault를 활용

실험1

Step 1.

01. UserDefault를 활용하여 사용자 Password 설정 저장하기

구동 화면

구현 코드

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 }
        if pwTextField.text == UserDefaults.standard.string(forKey: "Password") {
            diaryViewController.modalPresentationStyle = .fullScreen
            present(diaryViewController, animated: true)
        }
    }

    @IBAction func addNewPassword(_ sender: Any) {
        // UserDefaults을 활용해 패스워드를 저장합니다.
        UserDefaults.standard.set(pwTextField.text, forKey: "Password")
    }
}

02. KeyCahin을 활용하여 사용자 Password 설정 저장하기

실험2-1
구동 화면

kSecReturnData의 경우

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 } 
        // MARK: - Keychain을 활용해 패스워드를 호출합니다.
        guard let data = KeychainManager.get(account: "홍대로모이시조") else { return }

        let password = String(decoding: data, as: UTF8.self)

        if pwTextField.text == password {
            diaryViewController.modalPresentationStyle = .fullScreen
            present(diaryViewController, animated: true)
            pwTextField.text = ""
            print("ReadPassword: \(password)")
        }
    }

    @IBAction func addNewPassword(_ sender: Any) {
        // MARK: - Keychain을 활용해 패스워드를 저장합니다.
        do {
            try KeychainManager.save(account: "홍대로모이시조", password: pwTextField.text?.data(using: .utf8) ?? Data())
            pwTextField.text = ""
            print("비밀번호 저장되었습니다.")
        } catch {
            print("Error입니다.")
        }
    }
}

class KeychainManager {
    enum KeychainError: Error {
        case duplicateEntry
        case unkown(OSStatus)
    }

    static func save(account: String, password: Data) throws {
        let query: [String: AnyObject] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: account as AnyObject,
            kSecValueData as String: password as AnyObject,
        ]

        let status = SecItemAdd(query as CFDictionary, nil)

        if status == errSecDuplicateItem {
            try update(account: account, password: password)
            return
        }

        guard status == errSecSuccess else {
            throw KeychainError.unkown(status)
        }

        print("save")
    }

    static func update(account: String, password: Data) throws {
        let query: [String: AnyObject] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: account as AnyObject,
        ]

        let newQuery: [String: AnyObject] = [
            kSecValueData as String: password as AnyObject,
        ]

        let status = SecItemUpdate(query as CFDictionary, newQuery as CFDictionary)

        guard status == errSecSuccess else {
            throw KeychainError.unkown(status)
        }

        print("update")
    }

    static func get(account: String) -> Data? {
        let query: [String: AnyObject] = [
            kSecClass as String: kSecClassGenericPassword,
            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("ReadStatus: \(status)")

        return result as? Data
    }

    static func delete(account: String) {
        let query: [String: AnyObject] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: account as AnyObject,
        ]

        let status = SecItemDelete(query as CFDictionary)

        guard status == errSecSuccess else {
            print("delete fail")
        }

        print("delete success")
    }
}

kSecReturnAttributes의 경우

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 }
        // MARK: - Keychain을 활용해 패스워드를 호출합니다.
        guard let data = KeychainManager.get(account: "홍대로모이시조") else { return }

        let password = String(decoding: data, as: UTF8.self)

        if pwTextField.text == password {
            diaryViewController.modalPresentationStyle = .fullScreen
            present(diaryViewController, animated: true)
            pwTextField.text = ""
            print("ReadPassword: \(password)")
        }
    }

    @IBAction func addNewPassword(_ sender: Any) {
        // MARK: - Keychain을 활용해 패스워드를 저장합니다.
        do {
            try KeychainManager.save(account: "홍대로모이시조", password: pwTextField.text?.data(using: .utf8) ?? Data())
            pwTextField.text = ""
            print("비밀번호 저장되었습니다.")
        } catch {
            print("Error입니다.")
        }
    }
}

class KeychainManager {
    enum KeychainError: Error {
        case duplicateEntry
        case unkown(OSStatus)
    }
    static func save(account: String, password: Data) throws {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: account,
            kSecValueData as String: password,
        ]

        let status = SecItemAdd(query as CFDictionary, nil)

        if status == errSecDuplicateItem {
            try update(account: account, password: password)
            return
        }

        guard status == errSecSuccess else {
            throw KeychainError.unkown(status)
        }

        print("save")
    }

    static func update(account: String, password: Data) throws {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: account,
        ]

        let newQuery: [String: Any] = [
            kSecValueData as String: password,
        ]

        let status = SecItemUpdate(query as CFDictionary, newQuery as CFDictionary)

        guard status == errSecSuccess else {
            throw KeychainError.unkown(status)
        }

        print("update")
    }

    static func get(account: String) -> Data? {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: account,
            kSecReturnData as String: true,
            kSecMatchLimit as String: kSecMatchLimitOne,
            kSecReturnAttributes as String: true
        ]

        var result: AnyObject?
        let status = SecItemCopyMatching(query as CFDictionary, &result)

        if status == errSecSuccess, let attributes = result as? [String: Any],
              let passwordData = attributes[kSecValueData as String] as? Data {
               return passwordData
           }
        return nil
    }

    static func delete(account: String) {
        let query: [String: AnyObject] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: account as AnyObject,
        ]

        let status = SecItemDelete(query as CFDictionary)

        guard status == errSecSuccess else {
            print("delete fail")
        }

        print("delete success")
    }
}
실험2-2

Step 2.

SecItemCopyMatching 메서드의 두 매개변수를 확인해봅시다.

1. query는 무엇에 활용될까요?

  • 검색을 설명하는 사전입니다. 일반적인 쿼리 사전은 다음으로 구성됩니다.

    query

    let account: String = "password"
    let password = pwTextFireld.text else { return }
                    (비밀번호 값)
    var query: [String: Any] = [
        kSecClass as String: kSecClassInternetPassword,
        kSecAttrAccount as String: account,
        kSecAttrServer as String: server,
        kSecValueData as String: passwor
    ]

2. query 를 구성하는 요소들에는 무엇이 있을까요?

  • The item's class : Item Class Keys and Valus의 값 중 하나를 사용하여 원하는 아이템의 종류(예: 암호, 인증서 또는 암호화 키)를 지정합니다.
  • Attributes : 찾은 항목이 가져야 하는 속성을 표시하여 검색 범위를 좁힙니다. 지정하는 속성이 많을수록 결과가 더 세분화되지만 모든 항목 클래스에 모든 속성이 적용되는 것은 아닙니다. 가능한 속성의 전체 목록은 Item Attribute Keys and Values를 참조하십시오.
  • Search parameters : 다양한 방법으로 검색 조건을 지정합니다. 예를 들어 결과를 특정 수의 항목으로 제한하거나, 문자열 속성을 일치시킬 때 대소문자 구분을 제어하거나, 특정 항목 세트 중에서만 검색할 수 있습니다. 가능한 검색 매개변수의 전체 목록은 Search Attribute Keys and Values를 참조하십시오.
  • One or more return types : Item Return Result Keys에 있는 키를 사용하여 항목의 속성, 항목의 데이터, 데이터에 대한 참조, 데이터에 대한 영구 참조 또는 이들의 조합을 찾는지 여부를 나타냅니다.둘 이상의 반환 유형을 지정하면 요청한 각 유형이 포함된 사전이 검색에서 반환됩니다. 검색에서 여러 결과가 허용되면 모두 항목 배열로 함께 반환됩니다.

3. query의 key 종류 및 속성

  • Item Class Keys
    • kSecClass
      • 저장하려는 데이터의 종류를 의미합니다.
    • kSecAttr
      • 저장할 데이터의 종류를 지정해줍니다. 저장된 데이터를 검색할 때 필요한 정보입니다.
    • kSecValueData
      • 업데이트 할 값(Value)
    • kSecReturnAttrubutes
      • 값이 항목 속성을 반환할지 여부를 나타내는 값 입니다.

    참고 : Item Attribute Keys and Values

4. result는 무엇에 활용될까요?

  • 반환 시 찾은 항목에 대한 참조입니다. 결과의 정확한 유형은 Item Return Result Keys에서 설명한 대로 속성에 제공된 값을 기반으로 합니다.

Step 3.

구동 화면
class UpdatePasswordViewController: UIViewController {

    @IBOutlet weak var oldPassword: UITextField!
    @IBOutlet weak var newPassword: UITextField!
    
    @IBAction func updatePassword(_ sender: UIButton) {
        guard let data = KeychainManager.get(account: "홍대로모이시조") else { return }
        let password = String(decoding: data, as: UTF8.self)
        
        guard password == oldPassword.text else { return }
        try! KeychainManager.update(account: "홍대로모이시조", password: newPassword.text!.data(using: .utf8)!)
    }
}
참고링크