💫 KeychainManager 클래스 정의
final class KeychainManager { }
extension KeychainManager {
static var service: String { Bundle.main.bundleIdentifier ?? "" }
// TODO: 여기에 로직 정의
}
KeychainManager와 공통으로 사용할 serviceName을 정의한다.
Keychain에서는 account (Key)에 password (Value)를 저장해 놓고 사용하면 된다.
+) 에러타입 Enum
// Error
enum KeychainError: Error {
case duplicateItem // account에 데이터가 이미 존재
case noPassword // account에 데이터가 없음
case unexpectedPasswordData // 데이터 타입 오류
case unhandledError(status: OSStatus) // 기타 오류
}
에러는 간단하게 3가지와 기타 오류로 정의했다.
💫 데이터 추가하기
static func add(account: String, password: Data) throws {
let query: [CFString : Any] = [
kSecClass : kSecClassGenericPassword,
kSecAttrService : service,
kSecAttrAccount : account,
kSecValueData : password
]
let status = SecItemAdd(query as CFDictionary, nil)
guard status != errSecDuplicateItem else { throw KeychainError.duplicateItem }
guard status == errSecSuccess else { throw KeychainError.unhandledError(status: status) }
print("added!")
}
💫 데이터 불러오기
static func search(account: String) throws -> Data {
let query: [CFString : Any] = [
kSecClass : kSecClassGenericPassword,
kSecAttrService : service,
kSecAttrAccount : account,
kSecMatchLimit : kSecMatchLimitOne,
kSecReturnAttributes : true,
kSecReturnData : true
]
var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &item)
guard status != errSecItemNotFound else { throw KeychainError.noPassword }
guard status == errSecSuccess else { throw KeychainError.unhandledError(status: status) }
guard let existingItem = item as? [CFString : Any],
let passwordData = existingItem[kSecValueData] as? Data
else {
throw KeychainError.unexpectedPasswordData
}
print("searched!")
return passwordData
}
💫 데이터 수정하기
static func update(account: String, password: Data) throws {
let query: [CFString : Any] = [
kSecClass : kSecClassGenericPassword,
kSecAttrService : service,
kSecAttrAccount : account,
]
let attributes: [CFString : Any] = [
kSecValueData : password
]
let status = SecItemUpdate(query as CFDictionary, attributes as CFDictionary)
guard status != errSecItemNotFound else { throw KeychainError.noPassword }
guard status == errSecSuccess else { throw KeychainError.unhandledError(status: status) }
print("updated!")
}
static func delete(account: String) throws {
let query: [CFString : Any] = [
kSecClass : kSecClassGenericPassword,
kSecAttrService : service,
kSecAttrAccount : account
]
let status = SecItemDelete(query as CFDictionary)
guard status == errSecSuccess || status == errSecItemNotFound else { throw KeychainError.unhandledError(status: status) }
print("deleted!")
}
static func deleteAll() throws {
let query: [CFString : Any] = [
kSecClass : kSecClassGenericPassword,
kSecAttrService : service
]
let status = SecItemDelete(query as CFDictionary)
guard status == errSecSuccess || status == errSecItemNotFound else { throw KeychainError.unhandledError(status: status) }
print("deleted all!")
}
💫 사용해보기
// Add
do {
try KeychainManager.add(account: "Hello", password: "Hello".data(using: .utf8)!)
} catch {
// TODO: Error Handling
}
// Search
do {
var returnData: String = ""
let value = try KeychainManager.search(account: "Hello")
returnData = String(data: value, encoding: .utf8) ?? ""
} catch {
// TODO: Error Handling
}
Hello라는 Key값에 Hello라는 스트링 문자열을 저장하고 불러오는 로직이다.
do-catch로 인해서 코드가 길어질 뿐 아니라 공통되는 부분을 한번에 관리하기 힘들다는 점과 String타입을 저장하고 싶은데 Data밖에 안된다는 점, 또 add시에 이미 데이터가 있다면 알아서 update처리가 안된다는 점이 불편해서 UserDefaults스럽게 한번 더 래핑해봤다.
💫 메소드
extension KeychainManager {
static func set(_ data: Data, forKey account: String) {
do { try add(account: account, password: data) }
catch KeychainError.duplicateItem {
do { try update(account: account, password: data) }
catch { print("Keychain update error: \(error)") }
}
catch { print("Keychain add error: \(error)") }
}
// For String Type
static func set(_ string: String, forKey account: String) {
guard let sendingData = string.data(using: .utf8) else {
print("Keychain decoding error")
return
}
set(sendingData, forKey: account)
}
}
extension KeychainManager {
static func load(forKey account: String) -> Data? {
do { return try search(account: account) }
catch { print("Keychain search error: \(error)"); return nil }
}
// For String Type
static func string(forKey account: String) -> String? {
guard let loadedData = load(forKey: account) else { return nil }
return String(data: loadedData, encoding: .utf8)
}
}
extension KeychainManager {
static func remove(forKey account: String) {
do { try delete(account: account) }
catch { print("Keychain delete error: \(error)") }
}
static func removeAll() {
do { try deleteAll() }
catch { print("Keychain deleteAll error: \(error)") }
}
}
전체코드는 gist에서 확인!!
💫 다시 사용해보기
struct ContentView: View {
private let key1 = "key1"
private let key2 = "key2"
@State private var data1 = ""
@State private var data2 = ""
var body: some View {
VStack(spacing: 36) {
Text("Keychain Test")
.font(.largeTitle)
.bold()
VStack(alignment: .leading) {
HStack {
Button("SET 1") {
KeychainManager.set("Hello", forKey: key1)
}
Button("SET 2") {
KeychainManager.set("안녕", forKey: key1)
}
Button("LOAD") {
data1 = KeychainManager.string(forKey: key1) ?? "no data"
}
Button("REMOVE") {
KeychainManager.remove(forKey: key1)
}
}
Text("value 1: \(data1)")
.font(.title)
}
VStack(alignment: .leading) {
HStack {
Button("SET 1") {
KeychainManager.set("World!", forKey: key2)
}
Button("SET 2") {
KeychainManager.set("세상!", forKey: key2)
}
Button("LOAD") {
data2 = KeychainManager.string(forKey: key2) ?? "no data"
}
Button("REMOVE") {
KeychainManager.remove(forKey: key2)
}
}
Text("value 2: \(data2)")
.font(.title)
}
Button("REMOVE ALL") {
KeychainManager.removeAll()
}
}
.padding()
.buttonStyle(.borderedProminent)
}
}
'iOS > Swift' 카테고리의 다른 글
Swift OpenAPI Generator 사용해보기 (0) | 2024.03.29 |
---|---|
UserDefaults / Keychain / Core Data (0) | 2022.08.17 |
Swift에서 DI/DIP 이해하기 (0) | 2022.06.21 |
Swift CommonCrypto로 AES 암/복호화 하기! (without CryptoSwift) (0) | 2022.05.26 |
UserDefaults에서 .value(forKey:)와 .object(forKey:)는 뭐가 다른 건가요? (0) | 2022.05.13 |