사비성(sabisung)의 컴퓨터

40개의 항목
 

Keychain 샘플

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

실행 이미지

이미지 #1
이미지 #1

샘플 소스 다운로드

Sample Source Download (KeychainSample.zip)