버그 잡이

[iOS] Keychain Service 개념부터 사용법까지 알아보기 본문

IOS

[iOS] Keychain Service 개념부터 사용법까지 알아보기

버그잡이 2024. 3. 19. 22:35

 

Keychain이란?

 

Keychain은 iOS에서 제공하는 보안 저장소로, 사용자의 민감한 정보를 암호화하여 안전하게 저장할 수 있게 해줍니다.

이는 UserDefault나 파일 시스템 저장과 같은 다른 저장 방식보다 안전한 옵션을 제공합니다.

 

 

Keychain Item

 

keychain item 단위로 write / read / delete / update 합니다.

keychain item는 attribute를 통해서 key-value 값 뿐만 아니라 부가적인 데이터 저장 및 추가 옵션 설정이 가능합니다.

SecItemAdd(), SecItemDelete() 와 같은 함수을 통해서 keychain item을 저장할 수 있습니다.
(뒤에 나오는 코드를 보시면 보다 이해가 쉬울겁니다.)

 

Attributes

 

kSecAttrAccount: 저장되는 Key 값

kSecValueData: 저장되는 Value 값

kSecClass: 저장되는 데이터의 유형

  • kSecClassGenericPassword: 일반적인 비밀번호
  • kSecClassInternetPassword: 인터넷 서비스용 비밀번호

kSecAttrService : 서비스 이름

  • 같은 이름의 account라도 다른 service 값으로 저장할 수 있

kSecMatchLimit: 몇개의 데이터를 반환할 것이지.

  • kSecMatchLimitOne: 하나
  • kSecMatchLimitAll: 전부

kSecReturnData: 데이터를 반환할지 말지 여부

  • true일때 데이터 반환, false 는 관련 속성만 반환

kSecAttrAccessible: 키체인 데이터에 언제 접근할 수 있게 할지

  • kSecAttrAccessbileWhenUnlocked(default): 디바이스 잠금해제 후 사용가능
  • kSecAttrAccessibleAfterFirstUnlock: 사용자가 한번 잠금해제 하면 그 이후 접근 가능
  • 백그라운드에서 잠금 해제 하지 않고 키체인 아이템에 접근하려고 하면 접근이 안 될 수 있다.

kSecAttrSynchronizable: cloud 연동 여부

(추가 속성)

 

 

사용법

 

keychain 을 활용하여 어떻게 데이터를 저장하고 관리하는지 살펴보겠습니다.

(아래 코드는 제가 예전에 한 블로그에서 본 내용으로 재작성한건데 출처를 찾지 못하겠네요 ㅠ)

enum KeychainError: Error {
    case itemNotFound
    case duplicateItem
    case invalidItemFormat
    case unknown(OSStatus)
}

enum KeychainAccount: String {
    case password = "tistory_password"
}

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 get(account: String) throws -> String {
        try String(decoding: get(account: account), as: UTF8.self)
    }
    
    static func get(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: KeychainAccount) throws {
        let query: [String: AnyObject] = [
            kSecAttrService as String: service as AnyObject,
            kSecAttrAccount as String: account.rawValue as AnyObject,
            kSecClass as String: kSecClassGenericPassword
        ]
        
        let status = SecItemDelete(query as CFDictionary)
        
        guard status == errSecSuccess else {
            throw KeychainError.unknown(status)
        }
    }
}

 

기타 특성

  • 사용자가 직접 제거하지 않는 앱을 삭제하지 않아도 데이터는 남아있음
  • 같은 개발자가 만든 앱인 경우, 앱들 간의 키체인 공유 가능

 

키체인을 가져오지 못 하는 경우

  • 키체인이 사라지는 경우
    • iCloud로 백업 / 복원 시
  • kSecAttrAccessible 이 잠금상태 일때 접근 못 하도록 막은 경우
    • 접근이 안 되기 때문에 SecItemCopyMatching()을 해도 error가 남

 

 

 

참고

*https://developer.apple.com/documentation/security/keychain_services/

*https://beankhan.tistory.com/202

 

 

 

 

반응형
Comments