Keychain이란?
보안에 민감한 사용자 데이터, 비밀번호, 인증키, 기타 사용자 데이터를 OS에서 제공하는 일종의 보안 저장소이다.
물론, 앱에서 필요한 데이터를 UserDefault에 저장하고 사용할 수 있다. 그렇지만 보안에 민감한 데이터라면?
조금만(??) 노력하면 데이터를 훤히 들여다 볼 수 있다. ㅠㅠ
앱에서 암호화해서 데이터를 저장할 수도 있지만, 이미 OS에서는 이런 데이터를 저장할 수 있는 SDK와 저장소를 제공하므로 이를 사용하면 보다 더 사용자 데이터를 안전하게 관리할 수 있다.
ViewController.swif
//
// ViewController.swift
// KeychainSample
//
// Created by sabisung on 2021/07/15.
//
import UIKit
import Security
/// 항목 정의
struct BiometricMapping: Codable {
let logonId: String?
let deviceId: String?
}
class ViewController: UIViewController {
@IBOutlet weak var lblSuccess: UILabel!
@IBOutlet weak var lblErrorCode: UILabel!
@IBOutlet weak var lblErrorMesg: UILabel!
@IBOutlet weak var lblData: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
/// 추가 버튼
/// - Parameter sender: 버튼
@IBAction func itemAddTapped(_ sender: UIButton) {
guard let deviceId = UIDevice.current.identifierForVendor?.uuidString else {
return
}
let biometricMapping = BiometricMapping(logonId: "sabisung@gmail.com", deviceId: deviceId)
print("biometricMapping: \(biometricMapping)")
guard let data = try? JSONEncoder().encode(biometricMapping) else {
return
}
KeychainHelper().addItem(data) { (success, error) in
lblSuccess.text = success ? "성공!!" : "에러!!"
lblErrorCode.text = String(error?.status ?? 0)
lblErrorMesg.text = error?.localizedDescription ?? ""
lblData.text = ""
}
}
/// 읽기 버튼
/// - Parameter sender: 버튼
@IBAction func itemReadTapped(_ sender: UIButton) {
KeychainHelper().readItem { (data, error) in
if error == nil {
guard let data = data, let biometricMapping = try? JSONDecoder().decode(BiometricMapping.self, from: data) else {
return
}
print("biometricMapping: \(biometricMapping)")
lblData.text = "\(biometricMapping)"
}
lblSuccess.text = error == nil ? "성공!!" : "에러!!"
lblErrorCode.text = String(error?.status ?? 0)
lblErrorMesg.text = error?.localizedDescription ?? ""
}
}
/// 수정 버튼
/// - Parameter sender: 버튼
@IBAction func itemUpdateTapped(_ sender: UIButton) {
guard let deviceId = UIDevice.current.identifierForVendor?.uuidString else {
return
}
let biometricMapping = BiometricMapping(logonId: "yycho100@gmail.com", deviceId: deviceId)
print("biometricMapping: \(biometricMapping)")
guard let data = try? JSONEncoder().encode(biometricMapping) else {
return
}
KeychainHelper().updateItem(data) { (success, error) in
lblSuccess.text = success ? "성공!!" : "에러!!"
lblErrorCode.text = String(error?.status ?? 0)
lblErrorMesg.text = error?.localizedDescription ?? ""
lblData.text = ""
}
}
/// 삭제 버튼
/// - Parameter sender: 버튼
@IBAction func itemDeleteTapped(_ sender: UIButton) {
KeychainHelper().deleteItem() { (success, error) in
lblSuccess.text = success ? "성공!!" : "에러!!"
lblErrorCode.text = String(error?.status ?? 0)
lblErrorMesg.text = error?.localizedDescription ?? ""
lblData.text = ""
}
}
}
KeychainHelper.swif
//
// KeychainHelper.swift
// KeychainSample
//
// Created by sabisung on 2021/07/16.
//
import Foundation
import SwiftyJSON
/// Keychain 에러
struct KeychainError: Error {
var status: OSStatus
var localizedDescription: String {
return SecCopyErrorMessageString(status, nil) as String? ?? "Unknown error."
}
}
/// Keychain
class KeychainHelper {
private let service: String
private let account: String
/// 생성자
/// - Parameters:
/// - service: 서비스명
/// - account: 계정명
init(service: String, account: String) {
self.service = service
self.account = account
print("[KeychainHelper] service: \(self.service)")
print("[KeychainHelper] account: \(self.account)")
}
/// 생성자
convenience init() {
self.init(
service: Bundle.main.bundleIdentifier ?? "service",
account: (Bundle.main.bundleIdentifier?.split(separator: ".").last.map(String.init)) ?? "account"
)
}
/// 생성자
/// - Parameter account: 계정
convenience init(account: String) {
self.init(
service: Bundle.main.bundleIdentifier ?? "service",
account: account
)
}
/// 항목 추가
/// - Parameters:
/// - data: 항목 데이터
/// - completionHandler: 완료 핸들러
func addItem(_ data: Data, completionHandler: (Bool, KeychainError?) -> Void) {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account,
kSecAttrGeneric as String: data
]
let status = SecItemAdd(query as CFDictionary, nil)
guard status == errSecSuccess else {
completionHandler(false, KeychainError(status: status))
return
}
completionHandler(true, nil)
}
/// 항목 읽기
/// - Parameter completionHandler: 완료 핸들러
func readItem(_ completionHandler: (Data?, KeychainError?) -> Void) {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account,
kSecMatchLimit as String: kSecMatchLimitOne,
kSecReturnAttributes as String: true,
kSecReturnData as String: true
]
var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &item)
guard status == errSecSuccess else {
completionHandler(nil, KeychainError(status: status))
return
}
guard let existingItem = item as? [String: Any],
let data = existingItem[kSecAttrGeneric as String] as? Data else {
completionHandler(nil, KeychainError(status: errSecInternalError))
return
}
completionHandler(data, nil)
}
/// 항목 수정
/// - Parameters:
/// - data: 항목 데이터
/// - completionHandler: 완료 핸들러
func updateItem(_ data: Data, completionHandler: (Bool, KeychainError?) -> Void) {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account
]
let attributes: [String: Any] = [
kSecAttrAccount as String: account,
kSecAttrGeneric as String: data
]
let status = SecItemUpdate(query as CFDictionary, attributes as CFDictionary)
guard status == errSecSuccess else {
completionHandler(false, KeychainError(status: status))
return
}
completionHandler(true, nil)
}
/// 항목 삭제
/// - Parameter completionHandler: 완료 핸들러
func deleteItem(_ completionHandler: (Bool, KeychainError?) -> Void) {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account
]
let status = SecItemDelete(query as CFDictionary)
guard status == errSecSuccess else {
completionHandler(false, KeychainError(status: status))
return
}
completionHandler(true, nil)
}
}
실행 이미지