[iOS] 키체인 (Keychain Service)

안녕하세요~ 차니에요!

 

오늘은 키체인에 대해 알아보겠습니다~~!

 

1. 키체인이란?

Apple에서 공식으로 제공하는 보안 프레임워크이며, 사용자의 민감한(개인 정보 등) 데이터들을 저장하는 저장소입니다.

키체인은 사용자가 직접 제거하지 않는 한, 앱을 제거해도 키체인 데이터는 남아있고 디바이스가 lock되면 키체인도 함께 lock되며 디바이스가 unlock되면 키체인도 unlock된다는 특징이 있습니다.

 

키체인은 하나 이상의 Keychain Item을 갖습니다.

저장할 데이터의 종류(kSecClass)는 다음과 같습니다.

  • kSecClassGenericPassword : 일반 암호 항목을 나타내는 값입니다.
  • kSecClassInternetPassword : 인터넷 비밀번호 항목을 나타내는 값입니다.
  • kSecClassCertificate : 인증서 항목을 나타내는 값입니다.
  • kSecClassIdentity: ID 항목을 나타내는 값입니다.
  • kSecClassKey : 암호화 키 항목을 나타내는 값입니다.

보다 자세한 내용은 애플 공식 문서에서 확인해보실 수 있습니다.

 

2. 키체인 예제

대표적으로 많이 쓰이는 kSecClassGenericPassword 의 CRUD 예제를 준비해보았습니다.

 

2-1. Create - 키체인 생성

func addItem(key: Any, pwd: Any) -> Bool {
    let addQuery: [CFString: Any] = [kSecClass: kSecClassGenericPassword,
                                     kSecAttrAccount: key,
                                     kSecValueData: (pwd as AnyObject).data(using: String.Encoding.utf8.rawValue) as Any]
    
    let result: Bool = {
        let status = SecItemAdd(addQuery as CFDictionary, nil)
        if status == errSecSuccess {
            return true
        } else if status == errSecDuplicateItem {
            return updateItem(value: pwd, key: key)
        }
        
        print("addItem Error : \(status.description))")
        return false
    }()
    
    return result
}
addItem(key:pwd:) 키체인에 추가하는 예제 코드입니다. 
  1. 키체인 생성을 위한 쿼리(CFDictionary; addQuery)를 작성합니다.
    1. kSecClass : 데이터의 종류를 지정해줍니다. 
    2. kSecAttrAccount : 데이터 저장을 위한 키를 입력해줍니다.
    3. kSecValueData : 저장될 데이터를 Data Type으로 형변환하여 전달합니다.
  2. secItemAdd 함수 호출로 키체인에 추가하여 결과값을 받습니다.

키 값은 유일성을 갖고있어서 중복된 키 값으로 추가 시에 updateItem 함수를 수행하도록 추가하였습니다.

해당 부분은 2-3에서 다루도록 하겠습니다.

2-2. Read - 키체인 조회

func getItem(key: Any) -> Any? {
    let getQuery: [CFString: Any] = [kSecClass: kSecClassGenericPassword,
                                  kSecAttrAccount: key,
                                  kSecReturnAttributes: true,
                                  kSecReturnData: true]
    var item: CFTypeRef?
    let result = SecItemCopyMatching(getQuery as CFDictionary, &item)
    
    if result == errSecSuccess {
        if let existingItem = item as? [String: Any],
           let data = existingItem[kSecValueData as String] as? Data,
           let password = String(data: data, encoding: .utf8) {
            return password
        }
    }
    
    print("getItem Error : \(result.description)")
    return nil
}
getItem(key:) 키체인에서 조회하는 예제 코드입니다.
  1. 키체인 조회를 위한 쿼리(CFDictionary; getQuery)를 작성합니다.
    1. kSecClass : 데이터의 종류를 지정해줍니다. 
    2. kSecAttrAccount : 데이터 저장을 위한 키를 입력해줍니다.
    3. kSecReturnAttributes : 값이 항목 속성을 반환할지 여부를 나타내는 값 입니다.
    4. kSecReturnData : 값이 항목 데이터를 반환할지 여부를 나타내는 값 입니다.
  2. SecItemCopyMatching 함수 호출로 키체인에서 조회하여 결과값을 받습니다.

 

2-3. Update - 키체인 갱신

func updateItem(value: Any, key: Any) -> Bool {
    let prevQuery: [CFString: Any] = [kSecClass: kSecClassGenericPassword,
                                          kSecAttrAccount: key]
    let updateQuery: [CFString: Any] = [kSecValueData: (value as AnyObject).data(using: String.Encoding.utf8.rawValue) as Any]
    
    let result: Bool = {
        let status = SecItemUpdate(prevQuery as CFDictionary, updateQuery as CFDictionary)
        if status == errSecSuccess { return true }
        
        print("updateItem Error : \(status.description)")
        return false
    }()
    
    return result
}
updateItem(value:key:) 키체인에서 조회하는 예제 코드입니다.
  1. 이전 키체인 쿼리문 정보를 담아줍니다. (CFDictionary; prevQuery)
  2. 업데이트를 위한 쿼리(CFDictionary; updateQuery)를 작성합니다.
    1. kSecValueData : 업데이트 할 값(Value)
  3. SecItemUpdate 함수 호출로 해당 키 값의 키체인을 업데이트하여 결과값을 받습니다.

 

2-4. Delete - 키체인 삭제

func deleteItem(key: String) -> Bool {
    let deleteQuery: [CFString: Any] = [kSecClass: kSecClassGenericPassword,
                                        kSecAttrAccount: key]
    let status = SecItemDelete(deleteQuery as CFDictionary)
    if status == errSecSuccess { return true }
    
    print("deleteItem Error : \(status.description)")
    return false
}
deleteItem(key:) 키체인에서 조회하는 예제 코드입니다.
  1. 키체인 삭제를 위한 쿼리(CFDictionary; deleteQuery)를 작성합니다.
    1. kSecClass : 데이터의 종류를 지정해줍니다. 
    2. kSecAttrAccount : 키 값을 지정해줍니다.
  2. SecItemDelete 함수 호출로 해당 키 값의 키체인을 업데이트하여 결과값을 받습니다.

 

3. 전체 소스 코드

키체인을 CRUD 할 수 있는 클래스를 생성하여 싱글톤으로 사용하였습니다.

필요하신 분은 참고하시길 바랍니다!

더보기
class KeyChain {
        static let shared = KeyChain()
        
        func addItem(id: Any, pwd: Any) -> Bool {
            let addQuery: [CFString: Any] = [kSecClass: kSecClassGenericPassword,
                                             kSecAttrAccount: id,
                                             kSecValueData: (pwd as AnyObject).data(using: String.Encoding.utf8.rawValue) as Any]
            
            let result: Bool = {
                let status = SecItemAdd(addQuery as CFDictionary, nil)
                if status == errSecSuccess {
                    return true
                } else if status == errSecDuplicateItem {
                    return updateItem(value: pwd, key: id)
                }
                
                print("addItem Error : \(status.description))")
                return false
            }()
            
            return result
        }
        
        func getItem(key: Any) -> Any? {
            let query: [CFString: Any] = [kSecClass: kSecClassGenericPassword,
                                          kSecAttrAccount: key,
                                          kSecReturnAttributes: true,
                                          kSecReturnData: true]
            var item: CFTypeRef?
            let result = SecItemCopyMatching(query as CFDictionary, &item)
            
            if result == errSecSuccess {
                if let existingItem = item as? [String: Any],
                   let data = existingItem[kSecValueData as String] as? Data,
                   let password = String(data: data, encoding: .utf8) {
                    return password
                }
            }
            
            print("getItem Error : \(result.description)")
            return nil
        }
        
        func updateItem(value: Any, key: Any) -> Bool {
            let prevQuery: [CFString: Any] = [kSecClass: kSecClassGenericPassword,
                                                  kSecAttrAccount: key]
            let updateQuery: [CFString: Any] = [kSecValueData: (value as AnyObject).data(using: String.Encoding.utf8.rawValue) as Any]
            
            let result: Bool = {
                let status = SecItemUpdate(prevQuery as CFDictionary, updateQuery as CFDictionary)
                if status == errSecSuccess { return true }
                
                print("updateItem Error : \(status.description)")
                return false
            }()
            
            return result
        }
        
        func deleteItem(key: String) -> Bool {
            let deleteQuery: [CFString: Any] = [kSecClass: kSecClassGenericPassword,
                                                kSecAttrAccount: key]
            let status = SecItemDelete(deleteQuery as CFDictionary)
            if status == errSecSuccess { return true }
            
            print("deleteItem Error : \(status.description)")
            return false
        }
    }