IOS

[iOS] - 키체인(Key Chain)

clamp 2023. 6. 26. 11:38

키체인

 

 키체인을 학습하게된 이유는  OAuth를 통해 소셜로그인을 하고 toke을 받게된다. 이를 저장, 활용하려 하지만, 유저디폴트에 저장하기엔 민감한 정보라 키체인을 학습하게 되었다.

 

UserDefault와 다른점?

UserDefault에도 데이터를 쉽게 저장할 수 있지만 단순히 .info파일에 키-값 쌍의 텍스트 형태로 저장하게된다. 그렇기 때문에 OS를 탈옥하면 내용물을 볼 수 있기 때문에 보안이 필요한 민감한 데이터를 저장하기에는 보안이슈등 어울리지 않는다. 이를 방지하기 위해 암호, APIToken, 알고리즘을 위한 value등은 KeyChain에 저장하는 것이 좋다.

 

그렇다면 KeyChain이란 무엇일까?

  •  암호화된 데이터베이스, 즉 데이터를 안전하게 저장할 수 있는 보관소이다.

 

KeyChain의 특징은??

  • 앱을 삭제하더라도 Data는 삭제되지 않는다.
  • 정보와 속성으로 구성된다.
  • iOS app은 단일 키체인에 접근할 수 있다.
  • 사용자 기기 잠금 상태에 따라 키체인 잠금 상태도 동일하게 유지된다. -> 장치를 lock하면 key chain도 잠기고, unlock하면 keychain역시 풀린다.
  • 같은 개발자가 개발한 앱이라면 여러 앱에서 키체인 정보를 공유할 수 있다.

 

KeyChain에 무엇을 저장할까?

  • 로그인 암호
  • 결제데이터
  • 암호화 알고리즘을 위한 키
  • 간단한 메모?
  • 등등? 

 

KeyChain Service API

 

 

KeyChain Service API로 민감한 데이터를 암호화, 복호화 하며 재사용하기 쉽고 안전하게 사용할 수 있도록 도와준다.

 

 

Key Chain Items

  • Key Chain에 정보를 저장하기 위해서 KeyChain Item을 사용해야 한다.
  • 저장하려는 정보와 함께 item에 접근성을 제어하고 검색이 가능하게끔 하는 등 공개된 여러 특성(Attirbute)를 제공해야 한다.

 특성들의 예는 다음과 같다.

키 : kSecClass  

  • kSecClassGenericPassword: 일반적인 비밀번호
  • kSecClassCertificate: 인증서
  • kSecClassIdentity: iD

참고: https://developer.apple.com/documentation/security/keychain_services/keychain_items/item_class_keys_and_values

 

 

키: kSecAttrAccount

값:  데이터 저장을 위한 키 ex) com.devreels.authorization

 

키: kSecReturnData

값: 데이터를 리턴할지 true / false

 

키: kSecMatchLimit

  • kSecMatchLimitOne: 매치되는 1개의 데이터
  • kSecMatchIssuers: 값이 인증서 또는 ID발급자와 일치하는 데이터

참고: https://developer.apple.com/documentation/security/ksecmatchlimit

 

 

구현 코드 ( 키체인 매니저 )

struct Keychain: KeychainProtocol {
    
    // 주어진 query를 사용하여 Keychain에 항목을 추가한다.
    // query: Keychain에 추가할 항목에 대한 쿼리 파라미터
    // return: 작업의 성공 여부
    func add(_ query: [String: Any]) -> OSStatus {
        // Keychain에 항목을 추가하는 메서드.. query 정보를 기반으로 keychain에 새로운 항목이 추가된다.
        return SecItemAdd(query as CFDictionary, nil)
    }
    
    // Keychain에서 항목을 검색하는 메서드, query: 검색할 항목에 대한 쿼리 파라미터
    // 리턴은 Data or nil
    func search(_ query: [String: Any]) -> Data? {
        var item: CFTypeRef? // keychain에서 검색된 항목을 담을 변수
        // query를 기반으로 keychain에서 검색, &item을 전달하여 검색 결과를 item에 저장, status에는 검색의 성공 여부와 관련된 상태코드가 담긴다.
        let status = SecItemCopyMatching(query as CFDictionary, &item)
        return status == noErr ? (item as? Data) : nil // status가 성공이라면 item을 Data타입으로 캐스팅하여 리턴.
    }
    
    // query를 사용해 Keychain의 항목을 업데이트. attribute: 업데이트할 속성을 포함한 쿼리파라미터
    func update(_ query: [String: Any], with attributes: [String: Any]) -> OSStatus {
        return SecItemUpdate(query as CFDictionary, attributes as CFDictionary)
    }
    
    // query를 사용하여 Keychain에서 항목을 삭제, OSStatus로 삭제 작업의 성공 여부를 리턴
    func delete(_ query: [String: Any]) -> OSStatus {
        return SecItemDelete(query as CFDictionary)
    }
}
enum KeychainKey: String {
    case authorization = "com.devreels.authorization"
}

struct KeychainManager: KeychainManagerProtocol {

    var keychain: KeychainProtocol?

    func save(key: KeychainKey, data: Data) -> Bool {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: key.rawValue, // 데이터 저장을 위한 키
            kSecValueData as String: data   //저장될 데이터를 Data Type으로
        ]

        let status = keychain?.add(query)
        return status == errSecSuccess ? true : false // status가 errSecSuccess로 저장에 성공하면 true, else false
    }

    func load(key: KeychainKey) -> Data? {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: key.rawValue,
            kSecReturnData as String: true,             // 데이터를 리턴할지
            kSecMatchLimit as String: kSecMatchLimitOne //값이 일치하는 1개의 데이터만
        ]

        return keychain?.search(query)
    }

    func delete(key: KeychainKey) -> Bool {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: key.rawValue
        ]

        let status = keychain?.delete(query)
        return status == errSecSuccess ? true : false
    }

    func update(key: KeychainKey, data: Data) -> Bool {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: key.rawValue
        ]

        let attributes: [String: Any] = [
            kSecAttrAccount as String: key.rawValue,
            kSecValueData as String: data
        ]

        let status = keychain?.update(query, with: attributes)
        return status == errSecSuccess ? true : false
    }
}