키체인
키체인을 학습하게된 이유는 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
키: 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
}
}