애플사이다의 iOS 개발 일지

[Keychain] 키체인 사용하기 (예제 코드) 본문

iOS/영문 공식문서 뜯어보기-iOS

[Keychain] 키체인 사용하기 (예제 코드)

Applecider 2022. 8. 27. 00:26

저번 포스팅 [Keychain] 공식문서 읽는 방법 - 공식문서만으로 키체인을 이해해보자 에서 Keychain 이론을 다뤘다.

이제 간단한 예제를 통해 Keychain을 사용해보자.

 

*이 예제에서는 비밀번호를 저장하지만, 실제로는 사용자 인증 등 토큰을 저장하는 형태로 흔히 사용한다.


1. Keychain에 비밀번호 추가 (등록)하기

먼저 Keychain에 저장할 데이터 타입을 정의하자.

이번 예제에서는 password만 저장하지만, 다른 프로퍼티를 추가/저장하는 것도 가능하다.

struct Credentials {
//    var username: String  // 이번에는 필요 없음
    var password: String
}

 

Keychain 관련 Error 타입도 정의하자.

enum KeychainError: Error {
    case noPassword
    case unexpectedPasswordData
    case unhandledError(status: OSStatus)
}

 

이제 Keychain 타입을 정의하고, Keychain에 비밀번호를 추가하는 메서드를 구현하자.

struct Keychain {
    static func addItemsOnKeyChain(with password: String) {
        let credentials = Credentials(password: password)  // 저장할 비밀번호
        let password = credentials.password.data(using: String.Encoding.utf8)!  // pw를 암호화시킨 것

        // query (질의)를 통해 개발자는 keychain에게 질문을 함 
        // "이걸 Keychain에 보관해줄래?"
        // "kSecClassGenericPassword (일반적인 비밀번호)라는 query인데, 암호화해서 보관할 데이터는 password야"
        let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword,
//                                    kSecAttrAccount as String: account,  // 이번에는 필요 없음
                                    kSecValueData as String: password]  
        
        let status = SecItemAdd(query as CFDictionary, nil) 
        // "query를 던질건데 pw는 신경쓰지 않겠다, nil 대신 item에 담아서 서버로 보내겠다"
        // 기밀정보가 많다면, data 중에서 뭘 필요로 할지 필터링할 수 있도록 한 것

        if status == errSecSuccess {
            print("add success")
        } else if status == errSecDuplicateItem {
            print("keychain에 Item이 이미 있음")
        } else {
            print("add failed")
        }
    }
}
  • iOS에서는 번들마다 keychain을 부여한다. (번들은 앱이 가지고 있는 주머니)
  • 위에서 정의한 addItemsOnKeyChain 메서드를 통해 keychain의 kSecClassGenericPassword에 value가 추가된다.
  • 한 번 추가되면 동일한 내용을 다시 저장할 수 없다. (errSecDuplicateItem라는 OSStatus status가 발생한다.)
  • 저번 포스트의 공식문서에서 봤듯이 query (질의)는 Keychain에 저장할 Item이 어떤 종류의 정보인지 등을 나타낸다.
    Keychain에 Item을 add 할 때는 add에 맞는 질의를 하고, search를 할 때는 search에 맞는 질의를 해야 한다.
  • 완성된 query는 SecItemAdd 함수의 attributes 매개변수로 전달됐다.
    result 매개변수에는 nil이 전달됐다. SecItemAdd 메서드의 반환값이 필요 없다는 의미이다.

2. Keychain에 등록한 비밀번호 검색하기

static func searchItemFromKeychain() throws -> String? {
    let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword,
//                                kSecMatchLimit as String: kSecMatchLimitOne,
                                kSecReturnAttributes as String: true,  // search query에 Attribute가 있어야 원하는 값을 찾을 수 있음
                                kSecReturnData as String: true]

    var item: CFTypeRef?
    let status = SecItemCopyMatching(query as CFDictionary, &item)  // &item: 키체인 참조가 아님! 복사값을 넘김
    guard status != errSecItemNotFound else { throw KeychainError.noPassword }
    guard status == errSecSuccess else { throw KeychainError.unhandledError(status: status) }

    guard let existingItem = item as? [String : Any],  // 검색해서 찾은 Item을 타입 캐스팅해서 사용하면 됨
          let passwordData = existingItem[kSecValueData as String] as? Data,
          let password = String(data: passwordData, encoding: String.Encoding.utf8) else {
              throw KeychainError.unexpectedPasswordData
          }

    return password
}
  • 완성된 search query를 SecItemCopyMatching 메서드의 query 매개변수에 전달했다.
    result 매개변수에는 &item이 전달됐다. 
  • 검색 결과 Keychain에 저장된 Item을 찾았다면, 변수 item에 할당된다.
  • item을 타입 캐스팅하여 값에 접근할 수 있다.

3. Keychain에 등록한 비밀번호 업데이트하기

// Item을 업데이트한 뒤 결과에 따라 Alert를 띄울 예정
static func updateItemOnKeyChain(with password: String) -> UIAlertController { 
    let previousQuery: [String: Any] = [kSecClass as String: kSecClassGenericPassword]  // search query

    let newPW = password.data(using: .utf8)
    let updateQuery: [String: Any] = [kSecValueData as String: newPW]  // 업데이트할 Item
    let status = SecItemUpdate(previousQuery as CFDictionary, updateQuery as CFDictionary)

    if status == errSecSuccess {
        print("update complete")
        return AlertPresenter.showAlert(message: "비밀번호가 변경되었습니다")
    } else {
        print("not finished update")
        return AlertPresenter.showAlert(message: "주의 - 비밀번호 변경 중에 오류가 발생했습니다")
    }
}
  • 마찬가지로 update query가 필요하다.
  • SecItemUpdate 메서드를 활용한다.

 

- Reference

 

🍎 포스트가 도움이 되었다면, 공감🤍 / 구독🍹 / 공유🔗 / 댓글✏️ 로 응원해주세요. 감사합니다.

Comments