KeyChain
- 민가한 정보를 안전하게 저장하는 데 사용되는 iOS/macOS의 보안 기능
- 비밀번호, 인증 토큰, 사용자 인증 정보와 같은 데이터를 암호화된 형태로 안전하게 저장 가능
- Keychain Services API를 통해 정보를 저장하고 불어옴
- 키체인 항목을 추가(add), 검색(retrieve), 삭제(delete), 수정(modify) 기능을 사용함
https://developer.apple.com/documentation/security/storing-keys-in-the-keychain
주요 특징
- 보안성 : 데이터를 암호화해서 저장하므로, 데이터를 보호할 수 있음
- 앱 간 공유 : 동일한 개발자가 만든 앱끼리 Keychain을 공유할 수 있음
- Keychain Item의 속성 중 kSecAttrAccessGroup 속성
- 자동 관리 : iOS나 macOS가 Keychain의 저장 공간을 자동으로 관리
- 멀티 앱 지원 : 그룹 키체인으로 여러 앱에서 접근 가능한 키체인을 설정할 수 있음
Keychain의 기본 개념
- Keychain Item
- Keychain에 저장되는 데이터는 ‘아이템’으로 불림
- 아이템은 키와 값 쌍으로 저장
- Ex) 사용자 비밀번호를 저장할 때, Key, Value를 함께 저장
- Key : “user_password”
- Value : 비밀번호 값
- Keychain Class
- Keychain에는 여러 종류의 데이터 유형을 저장할 수 있음
- kSecClass라는 속성으로 데이터 유형을 저장
- 주요 데이터 유형으로는 비밀번호, 인증서, 공개/개인 키 등이 있음
- kSecClassGenericPassword : 일반 비밀번호를 저장할 때 사용
- kSecClassInternetPassword : 인터넷 비밀번호를 저장할 때 사용
- kSecClassCertificate : 인증서를 저장할 때 사용
- kSecClassKey : 암호화 키를 저장할 때 사용
- Security Framework 사용
- 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)
}
- @discardableResult
- https://docs.swift.org/swift-book/documentation/the-swift-programming-language/attributes/
- 함수나 메서드의 반환값이 사용되지 않아도 경고를 무시할 수 있도록 해주는 속성
- 메서드가 OSStatus를 반환하지만 사용하지 않아도 경고 무시
- https://developer.apple.com/documentation/security/item-class-keys-and-values
- kSecClass : Keychain에서 저장할 항목의 종류로 일반 비밀번호 (kSecClassGenericPassword) 지정
- kSecAttrAccount : Keychain에 저장할 계정 이름
- kSecAttrService : Keychain에 저장할 서비스 이름
- kSecValueData : 저장할 비밀번호를 Data 형태로 변환
- SecItemDelete(_ : CFDictionanry) : 중복 방지를 위해 동일한 쿼리로 저장된 값 삭제
- SecItemAdd(_:_:) : 쿼리 저장, 반환 값은 OSStatus
- https://developer.apple.com/documentation/security/secitemadd(_:_:)
- OSStatus
- https://developer.apple.com/documentation/security/security-framework-result-codes
- Security Framework에서 사용하는 상태 코드 타입 (함수 실행 결과에 따라 반환되는 코드)
- 작업의 성공 여부 또는 오류 상태를 정수형 타입으로 나타냄
- 성공 시 : errSecSuccess (값 : 0) 상수 반환
- 실패 시 : 에러 코드 반환
- 예)
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 : 검색 결과의 수 제한
- https://developer.apple.com/documentation/security/ksecmatchlimit
- kSecMatchLimitOne : 하나의 결과만 반환
- 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
- https://developer.apple.com/documentation/corefoundation/cftyperef
- CFTypeRef 타입은 AnyObject와 같은 타입
- Keychain에서 검색된 데이터가 다양한 타입일 수 있기 때문에 모든 타입을 받을 수 있는 AnyObject 타입을 사용
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
- https://developer.apple.com/documentation/security/secitemupdate(_:_:)
- Keychain에서 항목을 업데이트 하는 함수
- 반환 값으로 성공과 실패를 OSStauts 타입으로 나타냄
- 에러 처리 강화 (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)
}
- 매개변수
- account : 계정 이름
- service : 서비스 이름
- SecItemDelete : 주어진 계정과 서비스에 해당하는 Keychain 데이터 삭제
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 추출
- kSecAttrAccount
- https://developer.apple.com/documentation/security/ksecattraccount
- 항목의 account 항목의 계정 이름을 나타내는 문자열을 값으로 갖는 키 값
- kSecValueData
- https://developer.apple.com/documentation/security/ksecvaluedata
- 항목의 데이터를 값으로 갖는 키 값
- kSecAttrAccount
- 각각의 데이터를 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
'Swift' 카테고리의 다른 글
Swift - Struct와 Class 선택과 차이점 (0) | 2024.12.06 |
---|---|
Swift - UICollectionViewFlowLayout 셀 크기 다르게 구현 (UILabel) (0) | 2024.12.02 |
Swift - modal 여러 개 한 번에 dismiss 하기 (1) | 2024.11.20 |
Swift - 카카오 로그인 사용자 정보 가져오기 (1) | 2024.11.20 |
Swift - Configuration Settings File(.xcconfig 파일)을 이용한 git에 API Key 숨기기 (1) | 2024.11.16 |