Notice
Recent Posts
Recent Comments
Link
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
Archives
Today
Total
관리 메뉴

개발자의 삽질

[iOS] 키체인에 대해서 (서버에서 받은 token을 저장해보자) 본문

iOS

[iOS] 키체인에 대해서 (서버에서 받은 token을 저장해보자)

uniqueimaginate 2022. 5. 2. 16:46

https://developer.apple.com/documentation/security/keychain_services

 

Apple Developer Documentation

 

developer.apple.com


어떤 이유로 키체인에 대해 알아야 했을까?

현재 만들고 있는 앱은 서버에서 제공해준 카카오 OAuth을 통해 로그인하고 token을 받게 된다. 

처음에는 이를 UserDefault를 이용해 구현하려 했으나, 이는 보안상 좋은 방법이 아니라는 것을 알게되었고, 따라서 Keychain을 공부하게 되었다.

 

Keychain Services 에 대해서 알아보자

종종 사용자는 비밀 데이터를 안전하게 저장하고 싶어한다. 이 때 Keychain Services API가 도움을 줄 수 있다.

사용자는 자신의 비밀 데이터를 저장하고 싶을 때, Keychain이라 불리는 암호화된 데이터베이스에 정보를 저장할 수 있다.

 

Keychain은 비밀번호에만 국한되지 않는다.

아래의 그림과 같이

  1. 비밀번호
  2. 암호화된 키
  3. 인증서
  4. 짧은 메모

이렇게 4가지의 경우를 저장할 수 있습니다.

 

 

Keychain Items

Keychain에 비밀번호와 같은 정보를 저장하기 위해서는 keychain item을 사용해야 한다.

Keychain item을 사용할때는,

저장하려고 하는 정보와 함께 item에 접근성을 제어하고 검색 가능하게끔 하는 공개된 여러 특성들을 함께 제공해야 한다.

Item = Data + Attributes

Attributes 는 뒤에서 다시 설명이 나온다.

 

아래의 그림을 참고하자.

 

사용자의 비밀정보를 다루기 위해 사용하는 Keychain

Keychain의 가장 주된 목적 중 하나는, 비밀정보를 keychain에 저장함으로서 사용자가 일일히 기억하지 않게끔 도와주는 것이다.

For Good User Experience

 

아래는 Keychain을 이용해 인터넷 비밀번호를 저장하는 과정이다.

KeyChain CRUD

1. Create: 키체인에 비밀정보 추가하기

func SecItemAdd(_ attributes: CFDictionary, 
              _ result: UnsafeMutablePointer<CFTypeRef?>?) -> OSStatus

2. Read: 키체인에 저장된 Keychain Item 찾기

func SecItemCopyMatching(_ query: CFDictionary, 
                       _ result: UnsafeMutablePointer<CFTypeRef?>?) -> OSStatus

3. Update: 키체인에 저장된 정보 변경하기

func SecItemUpdate(_ query: CFDictionary, 
                 _ attributesToUpdate: CFDictionary) -> OSStatus

4. Delete: 키체인에 저장된 정보 삭제하기

func SecItemDelete(_ query: CFDictionary) -> OSStatus

 

자세히 들어가기 전에 먼저 Attributes에 대해서 다루어야 한다.

내가 생각하기에 Attributes 는 크게 3개의 정보를 갖고 있다.

  1. 어떤 종류의 비밀정보인가? (위에서 언급한 4가지의 경우) - Item Class Keys and Values
  2. Item은 어떠한 속성을 갖고 있는가? - Item Attribute Keys and Values
  3. 저장해둔 Keychain을 다시 찾기 위해서 어떤 고유값을 가져야 하는가? - Item Attribute Keys and Values

물론 크게 3개의 정보를 갖고 있다고 '제가' 생각을 하는 것이지 이는 해석에 따라 달라질 수 있다.

 

먼저 어떤 종류를 가지는지는 여러 특성키를 통해 나타내게 됩니다.

  • kSecClassGenericPassword - 비밀번호
  • kSecClassInternetPassword - 인터넷 비밀번호
  • kSecClassCertificate - 인증서
  • kSecClassIdentity - ID
  • kSecClassKey - 암호화된 키

이 5개가 위에서 내가 말한 어떤 종류의 비밀정보인가를 나타내게 된다.

 

나머지 2개, 즉 속성과 고유값 관련된 부분은 Item Attribute Keys and Values 링크를 따라가서 확인해보길 바란다.

양이 많아서 일일히 다루기는 비효율적인 것 같다.

 

아래 예시에서는

  • kSecClassGenericPassword 
  • kSecAttrService - 서비스 명시
  • kSecAttrAccount - 계정 명시
  • kSecValueData - 데이터 저장(type: Data)
  • kSecMatchLimit - 하나만 갖고 오기
  • kSecReturnData - 데이터 반환
  • kSecReturnAttributes - Attributes도 반환

를 사용한다.

 

 


 

Keychain Create

static let serviceName = "서비스이름"
static let account = "계정이름"

@discardableResult
static func create(token: String) -> Bool {
    let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword,
                                kSecAttrService as String: serviceName,
                                kSecAttrAccount as String: account,
                                kSecValueData as String: token.data(using: .utf8) as Any]

    let status = SecItemAdd(query as CFDictionary, nil)
    guard status == errSecSuccess else {
        print("Keychain create Error")
        return false
    }
    return true
}

Keychain Read

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

    var item: CFTypeRef?
    let status = SecItemCopyMatching(query as CFDictionary, &item)
    guard status != errSecItemNotFound else {
        print("Keychain item not found")
        return nil
    }
    guard status == errSecSuccess else {
        print("Keychain read Error")
        return nil
    }

    guard let existingItem = item as? [String: Any],
          let tokenData = existingItem[kSecValueData as String] as? Data,
          let token = String(data: tokenData, encoding: .utf8)
    else {
        print("Keychain get token Failed")
        return nil
    }
    return token
}

Keychain Update

@discardableResult
static func update(token: String) -> Bool{
    let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword]

    let attributes: [String: Any] = [kSecAttrService as String: serviceName,
                                     kSecAttrAccount as String: account,
                                     kSecValueData as String: token.data(using: .utf8) as Any]

    let status = SecItemUpdate(query as CFDictionary, attributes as CFDictionary)
    guard status != errSecItemNotFound else {
        print("Keychain item not found")
        return false
    }

    guard status == errSecSuccess else {
        print("Keychain update Error")
        return false
    }

    return true
}

Keychain Delete

@discardableResult
static func delete() -> Bool{
    let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword,
                                kSecAttrService as String: serviceName,
                                kSecAttrAccount as String: account]

    let status = SecItemDelete(query as CFDictionary)
    guard status == errSecSuccess || status == errSecItemNotFound else {
        print("Keychain delete Error")
        return false
    }

    return true
}

 

전체코드

import Foundation
import Security

class KeyChain {

    static let serviceName = "서비스이름"
    static let account = "계정이름"

    @discardableResult
    static func create(token: String) -> Bool {
        let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword,
                                    kSecAttrService as String: serviceName,
                                    kSecAttrAccount as String: account,
                                    kSecValueData as String: token.data(using: .utf8) as Any]

        let status = SecItemAdd(query as CFDictionary, nil)
        guard status == errSecSuccess else {
            print("Keychain create Error")
            return false
        }
        return true
    }

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

        var item: CFTypeRef?
        let status = SecItemCopyMatching(query as CFDictionary, &item)
        guard status != errSecItemNotFound else {
            print("Keychain item not found")
            return nil
        }
        guard status == errSecSuccess else {
            print("Keychain read Error")
            return nil
        }

        guard let existingItem = item as? [String: Any],
              let tokenData = existingItem[kSecValueData as String] as? Data,
              let token = String(data: tokenData, encoding: .utf8)
        else {
            print("Keychain get token Failed")
            return nil
        }
        return token
    }

    @discardableResult
    static func update(token: String) -> Bool {
        let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword]

        let attributes: [String: Any] = [kSecAttrService as String: serviceName,
                                         kSecAttrAccount as String: account,
                                         kSecValueData as String: token.data(using: .utf8) as Any]

        let status = SecItemUpdate(query as CFDictionary, attributes as CFDictionary)
        guard status != errSecItemNotFound else {
            print("Keychain item not found")
            return false
        }

        guard status == errSecSuccess else {
            print("Keychain update Error")
            return false
        }

        return true
    }

    @discardableResult
    static func delete() -> Bool {
        let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword,
                                    kSecAttrService as String: serviceName,
                                    kSecAttrAccount as String: account]

        let status = SecItemDelete(query as CFDictionary)
        guard status == errSecSuccess || status == errSecItemNotFound else {
            print("Keychain delete Error")
            return false
        }

        return true
    }

}

 

참고로 여기서는 kSecAttrService, kSecAttrAccount 를 이용해 구별을 했지만, 다른 방법을 사용해서 구별해도 되며,

하나의 앱에는 로그인 후에 하나의 user token을 사용하기 때문에 사실 유일하다.

따라서 위와 같이 따로 구별하지 않아도 될 것 같다.

오류처리

바로 위에서 delete 메서의 코드를 보면 status 변수가 있다.

let status = SecItemDelete(query as CFDictionary)

status 변수는 OSStatus 타입을 갖게 되는데, 에러 발생시 이를 그냥 출력해보면 정수만 나와서 어떤 에러가 나오는지 제대로 알수가 없다.

 

이 때, 아래의 함수를 사용한다면 유의미한 에러메시지를 보여준다. (print 를 이용해서 메시지를 보자)

SecCopyErrorMessageString(status, nil)

 

 

Comments