-
[iOS] Keychain 구현해보기👻 iOS 2021. 1. 25. 00:33
지난번 개론을 알아보는 포스트에 이어서.. 2021/01/24 - [👻 iOS] - [iOS] Keychain 개론
이번엔 실제 구현을 해봅니다.
간편하게 쓸 수 있는 라이브러리가 있긴하지만
공부하는 거니까 그냥 해볼께요.
1. 준비
화면이랑 연동해서 테스트를 해볼껀데 기본적인 컨트롤은 User ID, Password 입력 2개와 Load, Add, Update, Delete 를 실행하는 버튼 4개, 그리고 상태를 표시할 Label 1개를 선언해서 ViewController에 연결해 줄꺼예요.
프로젝트를 새로 만들고! (이때 신남)
service 값으로 사용할 멤버 변수를 하나 만들어서 Bundle ID를 반환하도록 하겠습니다.
class ViewController: UIViewController { @IBOutlet weak var userId: UITextField! @IBOutlet weak var password: UITextField! @IBOutlet weak var loadButton: UIButton! @IBOutlet weak var addButton: UIButton! @IBOutlet weak var updateButton: UIButton! @IBOutlet weak var deleteButton: UIButton! @IBOutlet weak var helpText: UILabel! var service: String = { guard let bundleIdentifier = Bundle.main.bundleIdentifier else { return "test.keychain" } return bundleIdentifier }() .... }
그리고 Storyboard 에서 순서대로 간단하게 연결해 줍니다.
2. Keychain에서 검색
화면에 User ID와 Password 를 입력한뒤 "Load Keychain"을 누르면
정의한 query대로 SecItemCopyMatching(_:_:) API 를 호출하게 됩니다.
query에 kSecClass는 웹이 아닌 앱이니까 kSecClassGenericPassword 로 선언하면 됩니다. (애플 문서는 대체로 Web기준으로...)
class ViewController: UIViewContorller { .... @IBAction func handleLoad(sender: UIButton) { guard let result = loadKeychain() else { helpText.text = "키체인을 불러오지 못했어요" return } helpText.text = "키체인을 불러왔어요." } private func loadKeychain() -> CFTypeRef? { guard let userIdValue = userId.text else { return nil } let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: userIdValue, kSecMatchLimit as String: kSecMatchLimitOne, kSecReturnAttributes as String: true, kSecReturnData as String: true] do { 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) } return item } catch let err { print("error", err) } return nil } .... }
3. Keychain으로 추가
형식은 이제 거의 비슷해요. 추가는 SecItemAdd(_:_:) API 를 사용하면 됩니다.
추가시에 필요한 것은 실제 저장할 Password 값이 추가된 정도?
대신 해당 값은 utf8 형식으로 인코딩해서 Data형으로 변환 후 저장해야 합니다.
class ViewController: UIViewController { .... @IBAction func handleAdd(sender: UIButton) { guard let userIdValue = userId.text else { return } guard let passwordValue = password.text else { return } let result = addKeychain(userIdValue: userIdValue, passwordValue: passwordValue) helpText.text = result ? "키체인 추가 성공" : "키체인 추가 실패" } private func addKeychain(userIdValue: String, passwordValue: String) -> Bool { // make query let account = userIdValue.trimmingCharacters(in: .whitespacesAndNewlines) let password = passwordValue.data(using: .utf8) as Any let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword, kSecAttrAccount as String: account, kSecAttrService as String: service, kSecValueData as String: password] // add item let status = SecItemAdd(query as CFDictionary, nil) do { guard status == errSecSuccess else { throw KeychainError.unhandledError(status: status) } return true } catch let err { print("error", err) } return false } .... }
4. Keychain으로 갱신
추가와 마찬가지로 User ID, Password 값을 기준으로 SecItemUpdate(_:_:) API를 사용해요.
거의 다른게 없으니까 이것도 패스!
class ViewController: UIViewController { .... @IBAction func handleUpdate(sender: UIButton) { guard let userIdValue = userId.text else { return } guard let passwordValue = password.text else { return } let result = updateKeychain(userIdValue: userIdValue, passwordValue: passwordValue) helpText.text = result ? "키체인 갱신 성공" : "키체인 갱신 실패" } private func updateKeychain(userIdValue: String, passwordValue: String) -> Bool { // make query let account = userIdValue.trimmingCharacters(in: .whitespacesAndNewlines) let password = passwordValue.data(using: .utf8) as Any let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service] let attributes: [String: Any] = [kSecAttrAccount as String: account, kSecValueData as String: password] // update item let status = SecItemUpdate(query as CFDictionary, attributes as CFDictionary) do { guard status != errSecItemNotFound else { throw KeychainError.noPassword } guard status == errSecSuccess else { throw KeychainError.unhandledError(status: status) } return true } catch let err { print("error", err) } return false } .... }
5. Keychain에서 삭제
삭제는 service와 User ID값만 있으면 됩니다. SecItemDelete(_:) API 를 사용해요.
class ViewController: UIViewController { .... @IBAction func handleDelete(sender: UIButton) { guard let userIdValue = userId.text else { return } let result = deleteKeychain(userIdValue: userIdValue) helpText.text = result ? "키체인 삭제 성공" : "키체인 삭제 실패" } private func deleteKeychain(userIdValue: String) -> Bool { // make query let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword, kSecAttrAccount as String: userIdValue, kSecAttrService as String: service] // delete item let status = SecItemDelete(query as CFDictionary) do { guard status == errSecSuccess || status == errSecItemNotFound else { throw KeychainError.unhandledError(status: status) } return true } catch let err { print("error", err) } return false } .... }
6. 저장 데이터 비교
그럼 Keychain 에서 데이터를 뽑아왔으면 가져와서 써야죠. Load 해서 얻은 item 을 로그로 찍어보면 아래와 같은 결과를 얻을 수 있어요. key는 요약해서 4자로 줄여쓴거 같은데 유심히 보면 뭔지 보입니다
그 중에 "v_Data" 라는게 그냥 봐도 "kSecValueData" 로 저장한 값이겠네 라고 추측되네요 ㅋㅋ
{ accc = "<SecAccessControlRef: ak>"; acct = "maart"; agrp = "XXXXXXXX.com.tistory.maart.keychain"; cdat = "2021-01-24 14:37:41 +0000"; mdat = "2021-01-24 14:52:08 +0000"; musr = {length = 0, bytes = 0x}; pdmn = ak; persistref = {length = 0, bytes = 0x}; sha1 = {length = 20, bytes = 0xdea4520daf83374d8fc59f1f8d3ed76f882bea42}; svce = "com.tistory.maart.keychain"; sync = 0; tomb = 0; "v_Data" = {length = 8, bytes = 0x7177657231323334}; }
item[kSecValueData] 로 값을 불러오면 utf8로 인코딩된 Data 값인데
이걸 String으로 다시 디코딩 해주면 입력했던 Password를 얻을 수 있어요.
간단하게 컨버팅 함수 하나 만들어서 변환해오도록 합니다.
class ViewController: UIViewController { .... private func convertKeychainDataToString(item: CFTypeRef) -> String? { do { guard let data = item[kSecValueData] as? Data else { throw KeychainError.unexpectedPasswordData } let str = String(decoding: data, as: UTF8.self) return str } catch let err { print("error", err) } return nil } }
그리고 아까 선언했던 불러오기 handler에 요걸 이용해서 안내 문구를 조금 더 친절하게 바꿔봅니다
class ViewController: UIViewController { .... @IBAction func handleLoad(sender: UIButton) { guard let result = loadKeychain() else { helpText.text = "키체인을 불러오지 못했어요" return } guard let keychainValue = convertKeychainDataToString(item: result) else { helpText.text = "키체인을 변환하지 못했어요" return } let passwordText = password.text?.trimmingCharacters(in: .whitespacesAndNewlines) let isMatching = keychainValue == passwordText helpText.text = "키체인을 불러왔어요.\n입력한 password와 \(isMatching ? "일치" : "불일치")" } .... }
이제 Load 를 하면 입력한 User ID 기준으로 불러오고 입력한 Password 와 keychain 데이터를 비교해서 화면에 일치 또는 불일치로 보여지게 될꺼에요.
7. 결론
Keychain을 이용하는 것은 보안상 중요한 정보, 그리고 앱이 속한 그룹간에 함께 사용할 보안정보를 사용하기에 꼭 필요한 기능이니 반드시 사용해 주어야 하는 중요한 기술이네요. 여태까지는 보안에 크게 중요성을 몰랐는데 이젠 처지가 달라지니 배워야할 것들도 많아요;;
keychain을 쓰기엔 query를 구성하는거 자체가 좀 귀찮은게 많은데, 찾아보니 괜찮은 라이브러리들이 많아요. 취향에 맞게 쓰는게 정신건강에도 좋겠습니다 ㅋㅋ
github.com/search?l=Swift&q=keychain&type=Repositories
(혹은 apple 의 샘플 프로젝트도 괜찮아요 : developer.apple.com/library/archive/samplecode/GenericKeychain/Introduction/Intro.html#//apple_ref/doc/uid/DTS40007797-Intro-DontLinkElementID_2 )
다만, 이렇게 키체인을 정말 간단히 써보긴 했는데 이렇게 저장해서 불러오는 구현을 해보면서 결국 이것도 암호화(해싱)해서 저장하고 비교해야 하는거 아닌가 하는 생각이 듭니다. 다음에 공부할 내용이 자연스럽게 나오네요 ㅠㅠ 갈 길이 멉니다 ㅠㅠㅠㅠ
다음엔 암호화에 대해서 조금 알아볼께요.