키체인이란 무엇인지, 어떻게 사용하는지 간단하게 설명합니다.
키체인(Keychain)이란?
- 암호화된 데이터베이스
- 즉, 데이터를 안전하게 저장할 수 있는 보관소
키체인 특징
- 앱을 삭제하더라도 데이터는 삭제되지 않음
- 키체인 아이템은 정보와 속성으로 구성됨
- iOS 앱은 단일 키체인에 접근할 수 있음
- 사용자 기기 잠금 상태에 따라 키체인 잠금 상태도 동일하게 유지됨
Keychain Service API
- 키체인이라는 암호화된 데이터베이스에 사용자 데이터를 안전하게 저장하는 API
- 단순한 비밀번호뿐 아니라 신용 카드 정보, 인증서 등 다양한 항목을 저장할 수 있음
Keychain Items
- 암호나 암호화 키와 같은 비밀을 저장하려면 키체인 항목으로 패키징
- 키체인 아이템은 데이터 자체와 함께 항목의 액세스 가능성을 제어하고 검색 가능하게 만들기 위해 공개적으로 표시되는 속성 집합
- 아래 그림과 같이 키체인 서비스는 디스크에 저장된 키체인에서 데이터 암호화 및 저장을 처리
Item Class Keys and Values
- kSecClass : 키체인 아이템의 타입
- kSecClassGenericPassword : 일반 비밀번호를 저장할 때 사용
- kSecClassInternetPassword : 인터넷용 아이디/패스워드를 저장할 때 사용
- kSecClassCertificate : 인증서를 저장할 때 사용
- kSecClassKey
- kSecClassIdentity
Attributes
Item Class에 따라 설정할 수 있는 attributes가 다르며 항목이 많기 때문에 애플 문서를 참고하면 좋을 듯하다.
General Item Attribute Keys
- kSecAttrAccess
- kSecAttrAccessControl
- kSecAttrAccessGroup
- ..
키체인 주요 API
- 키체인 아이템 추가
func SecItemAdd(CFDictionary, UnsafeMutablePointer<CFTypeRef?>?) -> OSStatus
- 키체인 아이템 조회
func SecItemCopyMatching(CFDictionary, UnsafeMutablePointer<CFTypeRef?>?) -> OSStatus
- 키체인 아이템 업데이트
func SecItemUpdate(CFDictionary, CFDictionary) -> OSStatus
- 키체인 아이템 삭제
func SecItemDelete(CFDictionary) -> OSStatus
키체인 사용 방법
단순하게 account를 key로 아이템을 추가/조회/업데이트/삭제하는 방법이다.
- 키체인 아이템 추가
isForce 값에 따라 true인 경우는 기존에 저장된 아이템이 있는 경우 update를 진행한다.
static func save(account: String, value: Data, isForce: Bool = false) throws {
let query: [String: AnyObject] = [
kSecAttrService as String: service as AnyObject,
kSecAttrAccount as String: account as AnyObject,
kSecClass as String: kSecClassGenericPassword,
kSecValueData as String: value as AnyObject,
]
let status = SecItemAdd(query as CFDictionary, nil)
if status == errSecDuplicateItem {
if isForce {
try update(account: account, value: value)
return
} else {
throw KeychainError.duplicateItem
}
}
guard status == errSecSuccess else {
throw KeychainError.unknown(status)
}
}
- 키체인 아이템 조회
static func load(account: String) throws -> Data {
let query: [String: AnyObject] = [
kSecAttrService as String: service as AnyObject,
kSecAttrAccount as String: account as AnyObject,
kSecClass as String: kSecClassGenericPassword,
kSecMatchLimit as String: kSecMatchLimitOne,
kSecReturnData as String: kCFBooleanTrue,
]
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
guard status != errSecItemNotFound else {
throw KeychainError.itemNotFound
}
guard status == errSecSuccess else {
throw KeychainError.unknown(status)
}
guard let password = result as? Data else {
throw KeychainError.invalidItemFormat
}
return password
}
- 키체인 아이템 업데이트
static func update(account: String, value: Data) throws {
let query: [String: AnyObject] = [
kSecAttrService as String: service as AnyObject,
kSecAttrAccount as String: account as AnyObject,
kSecClass as String: kSecClassGenericPassword,
kSecValueData as String: value as AnyObject,
]
let attributes: [String: AnyObject] = [
kSecValueData as String: value as AnyObject
]
let status = SecItemUpdate(query as CFDictionary, attributes as CFDictionary)
guard status != errSecDuplicateItem else {
throw KeychainError.duplicateItem
}
guard status == errSecSuccess else {
throw KeychainError.unknown(status)
}
}
- 키체인 아이템 삭제
static func delete(account: String) throws {
let query: [String: AnyObject] = [
kSecAttrService as String: service as AnyObject,
kSecAttrAccount as String: account as AnyObject,
kSecClass as String: kSecClassGenericPassword
]
let status = SecItemDelete(query as CFDictionary)
guard status == errSecSuccess else {
throw KeychainError.unknown(status)
}
}
+ 키체인 에러 정의
enum KeychainError: Error {
case itemNotFound
case duplicateItem
case invalidItemFormat
case unknown(OSStatus)
}
전체 코드
// KeychainManager.swift
enum KeychainError: Error {
case itemNotFound
case duplicateItem
case invalidItemFormat
case unknown(OSStatus)
}
class KeychainManager {
static let service = Bundle.main.bundleIdentifier
// MARK: - Save
static func save(account: String, value: String, isForce: Bool = false) throws {
try save(account: account, value: value.data(using: .utf8)!, isForce: isForce)
}
static func save(account: String, value: Data, isForce: Bool = false) throws {
let query: [String: AnyObject] = [
kSecAttrService as String: service as AnyObject,
kSecAttrAccount as String: account as AnyObject,
kSecClass as String: kSecClassGenericPassword,
kSecValueData as String: value as AnyObject,
]
let status = SecItemAdd(query as CFDictionary, nil)
if status == errSecDuplicateItem {
if isForce {
try update(account: account, value: value)
return
} else {
throw KeychainError.duplicateItem
}
}
guard status == errSecSuccess else {
throw KeychainError.unknown(status)
}
}
// MARK: - Update
static func update(account: String, value: String) throws {
try update(account: account, value: value.data(using: .utf8)!)
}
static func update(account: String, value: Data) throws {
let query: [String: AnyObject] = [
kSecAttrService as String: service as AnyObject,
kSecAttrAccount as String: account as AnyObject,
kSecClass as String: kSecClassGenericPassword,
kSecValueData as String: value as AnyObject,
]
let attributes: [String: AnyObject] = [
kSecValueData as String: value as AnyObject
]
let status = SecItemUpdate(query as CFDictionary, attributes as CFDictionary)
guard status != errSecDuplicateItem else {
throw KeychainError.duplicateItem
}
guard status == errSecSuccess else {
throw KeychainError.unknown(status)
}
}
// MARK: - Load
static func load(account: String) throws -> String {
try String(decoding: load(account: account), as: UTF8.self)
}
static func load(account: String) throws -> Data {
let query: [String: AnyObject] = [
kSecAttrService as String: service as AnyObject,
kSecAttrAccount as String: account as AnyObject,
kSecClass as String: kSecClassGenericPassword,
kSecMatchLimit as String: kSecMatchLimitOne,
kSecReturnData as String: kCFBooleanTrue,
]
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
guard status != errSecItemNotFound else {
throw KeychainError.itemNotFound
}
guard status == errSecSuccess else {
throw KeychainError.unknown(status)
}
guard let password = result as? Data else {
throw KeychainError.invalidItemFormat
}
return password
}
// MARK: - Delete
static func delete(account: String) throws {
let query: [String: AnyObject] = [
kSecAttrService as String: service as AnyObject,
kSecAttrAccount as String: account as AnyObject,
kSecClass as String: kSecClassGenericPassword
]
let status = SecItemDelete(query as CFDictionary)
guard status == errSecSuccess else {
throw KeychainError.unknown(status)
}
}
}
참고 URL
- https://developer.apple.com/documentation/security/keychain_services
'iOS > Swift + Objective-c' 카테고리의 다른 글
[Swift] UITableView cell 선택하기 (프로그래밍 방식) (0) | 2023.03.08 |
---|---|
[Swift / Objective-c] 디바이스 USIM 확인하기 (유심 확인) (0) | 2022.12.20 |
[Swift] UITabBarController 이미지 설정 (0) | 2022.08.29 |
[Swift] FileManager로 디바이스에 파일 저장하기 (Document) (0) | 2022.08.04 |
[Objective-c] SQL Cipher 사용하여 db 암호화하는 방법 (0) | 2022.06.20 |