iOS/Swift + Objective-c

[Swift] 키체인 설명 및 사용 방법 (Keychain)

안경 쓴 귀니 2022. 9. 28. 23:19
반응형

키체인이란 무엇인지, 어떻게 사용하는지 간단하게 설명합니다.

 

키체인(Keychain)이란?

- 암호화된 데이터베이스

- 즉, 데이터를 안전하게 저장할 수 있는 보관소

 

키체인 특징

- 앱을 삭제하더라도 데이터는 삭제되지 않음

- 키체인 아이템은 정보와 속성으로 구성됨

- iOS 앱은 단일 키체인에 접근할 수 있음

- 사용자 기기 잠금 상태에 따라 키체인 잠금 상태도 동일하게 유지됨

 

Keychain Service API

- 키체인이라는 암호화된 데이터베이스에 사용자 데이터를 안전하게 저장하는 API

- 단순한 비밀번호뿐 아니라 신용 카드 정보, 인증서 등 다양한 항목을 저장할 수 있음

키체인에 안전하게 저장되는 정보들

 

Keychain Items

- 암호나 암호화 키와 같은 비밀을 저장하려면 키체인 항목으로 패키징

- 키체인 아이템은 데이터 자체와 함께 항목의 액세스 가능성을 제어하고 검색 가능하게 만들기 위해 공개적으로 표시되는 속성 집합

- 아래 그림과 같이 키체인 서비스는 디스크에 저장된 키체인에서 데이터 암호화 및 저장을 처리

Data와 attributes를 키체인에 넣는 방법

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

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

 

반응형