본문 바로가기

iOS/Swift

Keychain을 사용해보자

💫 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!")
}

 

ref) https://developer.apple.com/documentation/security/keychain_services/keychain_items/adding_a_password_to_the_keychain

 

💫 데이터 불러오기

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
}

ref) https://developer.apple.com/documentation/security/keychain_services/keychain_items/searching_for_keychain_items

 

💫 데이터 수정하기

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!")
}

ref) https://developer.apple.com/documentation/security/keychain_services/keychain_items/updating_and_deleting_keychain_items

 

💫 사용해보기

// 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)
    }
}