Swift

Swift - KeyChain이란?

Goniii 2024. 11. 24. 17:56

KeyChain

  • 민가한 정보를 안전하게 저장하는 데 사용되는 iOS/macOS의 보안 기능
  • 비밀번호, 인증 토큰, 사용자 인증 정보와 같은 데이터를 암호화된 형태로 안전하게 저장 가능
  • Keychain Services API를 통해 정보를 저장하고 불어옴
    • 키체인 항목을 추가(add), 검색(retrieve), 삭제(delete), 수정(modify) 기능을 사용함

https://developer.apple.com/documentation/security/storing-keys-in-the-keychain

주요 특징

  1. 보안성 : 데이터를 암호화해서 저장하므로, 데이터를 보호할 수 있음
  2. 앱 간 공유 : 동일한 개발자가 만든 앱끼리 Keychain을 공유할 수 있음
    1. Keychain Item의 속성 중 kSecAttrAccessGroup 속성
  3. 자동 관리 : iOS나 macOS가 Keychain의 저장 공간을 자동으로 관리
  4. 멀티 앱 지원 : 그룹 키체인으로 여러 앱에서 접근 가능한 키체인을 설정할 수 있음

Keychain의 기본 개념

  1. Keychain Item
    • Keychain에 저장되는 데이터는 ‘아이템’으로 불림
    • 아이템은 키와 값 쌍으로 저장
    • Ex) 사용자 비밀번호를 저장할 때, Key, Value를 함께 저장
      • Key : “user_password”
      • Value : 비밀번호 값
  2. Keychain Class
    • Keychain에는 여러 종류의 데이터 유형을 저장할 수 있음
    • kSecClass라는 속성으로 데이터 유형을 저장
    • 주요 데이터 유형으로는 비밀번호, 인증서, 공개/개인 키 등이 있음
      • kSecClassGenericPassword : 일반 비밀번호를 저장할 때 사용
      • kSecClassInternetPassword : 인터넷 비밀번호를 저장할 때 사용
      • kSecClassCertificate : 인증서를 저장할 때 사용
      • kSecClassKey : 암호화 키를 저장할 때 사용
  3. Security Framework 사용
  4. import Security

 

1. Keychain 저장 함수

    // Keychain에 데이터를 저장하는 함수
    @discardableResult // 반환 값 사용에 대한 경고 무시 (반환 된 OSStatus 값을 사용하지 않아도 경고가 뜨지 않음)
    func save(account: String, service: String = "login", password: String) -> OSStatus {
        let passwordData = password.data(using: .utf8)!  // 저장할 데이터: 문자열을 Data로 변환
        
        // 저장할 데이터와 속성을 포함한 쿼리 구성
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,  // 비밀번호 저장
            kSecAttrAccount as String: account,             // 계정 이름
            kSecAttrService as String: service,             // 서비스 이름
            kSecValueData as String: passwordData           // 실제 비밀번호 데이터
        ]
        
        // 기존 항목이(같은 키) 있으면 삭제
        SecItemDelete(query as CFDictionary)
        
        // 새로운 데이터 저장
        return SecItemAdd(query as CFDictionary, nil)
    }
let status = SecItemAdd(query as CFDictionary, nil)

if status == errSecSuccess {
    print("Item successfully added to Keychain")
} else {
    print("Failed to add item: \\(status)")
}

 

2. Keychain 검색 함수

    // Keychain에서 데이터를 불러오는 함수
    func load(account: String, service: String = "login") -> String? {
		    // 검색할 속성 정의
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,  // 비밀번호 클래스 사용
            kSecAttrAccount as String: account,             // 계정 이름
            kSecAttrService as String: service,             // 서비스 이름
            kSecReturnData as String: true,                 // 데이터를 반환하도록 설정
       //   kSecReturnData as String: kCFBooleanTrue!,      // 데이터를 반환하도록 설정
            kSecMatchLimit as String: kSecMatchLimitOne     // 하나의 결과만 반환
        ]
        
        // CFTypeRef 타입 == AnyObject 타입
        var item: CFTypeRef?
        // var item : AnyObject? = nil
        
        // 데이터 검색
        let status : OSStatus = SecItemCopyMatching(query as CFDictionary, &item)
        
        // 검색의 성공, 실패 확인 -> 실패면 return nil
        guard status == errSecSuccess else { return nil }
        
        if let data = item as? Data, let password = String(data: data, encoding: .utf8) {
            return password
        }
        
        return nil
    }
  • 매개변수
    • account : 계정 이름
    • service : 서비스 이름
  • kSectReturnData : 데이터를 반환하도록 설정 (Data 타입)
  • kSecMatchLimit : 검색 결과의 수 제한
  • SecItemCopyMatching : Keychain에서 매칭되는 데이터를 가져오는 함수
    • https://developer.apple.com/documentation/security/secitemcopymatching(_:_:)
    • 성공하면 데이터를 CFTypeRef 타입으로 반환
    • query : 첫 번째 파라미터로 검색 조건 구성
    • &item : 두 번째 파라미터로 변수의 주소를 전달하여 검색된 데이터를 item 변수에 저장
      • &item은 in-out 파라미터로 SecItemCopyMatching 함수가 item의 메모리 주소에 사용해 데이터를 직접 수정하고 반환하는 구조
      • item 변수는 호출 시 비어있지만, 함수 호출 후 Keychain에서 찾은 데이터가 저장됨
  • guard status == errSecSuccess else { return nil }
    • 검색 성공 시 status의 값은 errSecSuccess가 되고 item 변수에는 검색된 데이터가 저장됨
    • 검색 실패 시 status의 값은 오류 코드가 되고 item은 nil로 남음
      • 따라서 status == errSecSuccess 즉, 검색 성공이면 다음 코드를 진행하고 실패면 nil 리턴
  • guard status == errSecSuccess else { return nil }
  • CFTypeRef

 

3. Keychain 수정 함수

   // Keychain에서 데이터를 업데이트하는 함수
    @discardableResult
    func update(account: String, service: String = "login", newPassword: String) -> OSStatus {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: account,
            kSecAttrService as String: service
        ]
        
        let attributesToUpdate: [String: Any] = [
            kSecValueData as String: newPassword.data(using: .utf8)!
        ]
        
        return SecItemUpdate(query as CFDictionary, attributesToUpdate as CFDictionary)
    }
  • 매개변수
    • account : 계정 이름
    • service : 서비스 이름
    • newPassword : 새 비밀번호
  • query : Keychain에서 업데이트 할 항목을 찾기 위한 조건을 정의하는 딕셔너리
  • attributesToUpdate : 찾은 항목에 대해 업데이트할 속성을 정의하는 딕셔너리
  • SecItemUpdate
  • 에러 처리 강화 (OSStatus)
let status = update(account: account, newPassword: newPassword)

switch status {
case errSecSuccess:
    print("업데이트 성공")
case errSecItemNotFound:
    print("해당 계정의 항목을 찾을 수 없음")
case errSecAuthFailed:
    print("인증 실패")
default:
    print("알 수 없는 오류 발생: \(status)")
}

 

4. Keychain 삭제 함수

    // Keychain에서 데이터를 삭제하는 함수
    @discardableResult
    func delete(account: String, service: String) -> OSStatus {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,  // 비밀번호 클래스 사용
            kSecAttrAccount as String: account,             // 계정 이름
            kSecAttrService as String: service              // 서비스 이름
        ]
        
        // 해당하는 Keychain Item 삭제
        return SecItemDelete(query as CFDictionary)
    }

 

5. Keychain 모두 검색 함수

    // Keychain 데이터 모두 가져오는 함수
    @discardableResult
    func all(service : String = "login") -> [KeychainModel]? {
        let query : [String : Any] = [
            kSecClass as String : kSecClassGenericPassword,
            kSecAttrService as String : service,
            kSecReturnAttributes as String : true,  // 속성 반환
            kSecReturnData as String: true,         // 데이터 반환
            kSecMatchLimit as String : kSecMatchLimitAll
        ]
        
        var item : CFTypeRef?
        let status : OSStatus = SecItemCopyMatching(query as CFDictionary, &item)
        
        // 검색의 성공, 실패 확인 -> 실패면 return nil
        guard status == errSecSuccess, let data = item as? [[String: Any]] else { return nil }
        
        // Keychain에서 가져온 데이터를 KeychainModel로 변환
       var keychainModels: [KeychainModel] = []
       
       for dic in data {
           if let account = dic[kSecAttrAccount as String] as? String,
              let passwordData = dic[kSecValueData as String] as? Data,
              let password = String(data: passwordData, encoding: .utf8) {
               let model = KeychainModel(email: account, password: password)
               keychainModels.append(model)
           }
       }
       
       return keychainModels
    }
  • KeychainModel : email과 password를 담을 구조체
  • 쿼리 구성
    • email과 password 모두 필요로 하므로 kSecReturnAttributes, kSecReturnData 속성 추가
    • 모든 항목을 반환하기 때문에 kSecMatchLimitAll 사용
  • SecItemCopyMatching 함수로 모든 항목 가져옴
  • 모든 데이터 항목에 관하여 account와 passwordData 추출
  • 각각의 데이터를 KeychainModel로 변환하여 keychainModels에 저장 후 반환

 

 

 

Example Project

https://github.com/LeeeeSuHyeon/Keychain_Example

 

GitHub - LeeeeSuHyeon/Keychain_Example: Keychain_Example

Keychain_Example. Contribute to LeeeeSuHyeon/Keychain_Example development by creating an account on GitHub.

github.com