Keychain
애플에서 사용자 데이터를 안전하게 저장할 수 있도록 제공하는 암호화된 데이터베이스
Keychain Service API를 사용하면 키체인 안에 사용자 데이터를 저장할 수 있습니다.
- 안전하게 저장해야 하는 작은 데이터를 저장할 때 사용 - ex) 비밀번호, 암호키, 인증키, 메모 등
- SandBox 밖에 데이터가 저장되기 때문에 앱을 삭제해도 데이터가 남아있음
- 디바이스가 Lock/Unlock되면 키체인도 같이 Lock/Unlock됨
- Secure Enclave라는 암호화 기능을 사용하는 하드웨어에서 키체인 데이터를 관리함
- Keychain Sharing을 통해 동일한 개발자에 속한 서로 다른 앱 간 키체인 공유가 가능

📌 UserDefaults와 비교
Keychain은 UserDefaults와 대체로 유사하지만 다른 점이 몇 가지 있습니다.
- UserDefaults로 저장되는 데이터는 plist에 저장되기 때문에 보안에 취약함
- UserDefaults에 저장된 데이터는 앱 삭제 시 같이 삭제됨
- UserDefaults는 앱이 시작될 때 plist 파일이 로드되어 싱글톤 형태로 접근하기 때문에, 데이터가 클 수록 로드 시 성능 저하가 발생
- UserDefaults는 App Group을 통해 Extension 간 데이터 공유가 가능
👉 그렇기 때문에 유저의 로그인 정보와 같이 민감한 정보들은 UserDefaults가 아닌 Keychain에 저장해주는 것이 좋습니다.
Keychain Item
키체인에 데이터를 저장할 때는 Keychain Item이라는 형태로 저장하게 됩니다.
즉, Keychain Item은 키체인에 저장하는 Data의 단위라고 할 수 있습니다.
- 비밀번호, 암호화 키 같은 정보를 키체인에 저장하고 싶을 때 Keychain Item으로 패키징
- Keychain Item으로 패키징하면 데이터 자체(data) 뿐만 아니라, item의 접근성을 제어하고 검색할 수 있도록 public하게 볼 수 있는 arribute들이 제공됨
- 키체인 서비스는 키체인에서 data, attribute 모두에 대해 데이터 암호화 및 저장을 처리
- 👉 나중에 승인된 프로세스는 키체인 서비스를 사용하여 item을 찾고, 데이터를 복호화할 수 있습니다.

📌 Keychain Item class의 key-value
Keychain item은 password(비밀번호), cryptographic key(암호화 키), certificate(인증서) 등과 같이 저장하려는 데이터의 종류에 따라 다양한 class로 제공됩니다. (Swift 문법적 의미의 class가 아님)
item이 어떤 class인지에 따라서 적용 가능한 attribute와, 시스템을 통한 데이터의 암호화 여부 등이 결정됩니다.
키체인에서 item에 대한 CRUD 작업이 이루어질 때는 딕셔너리 형태로 쿼리를 만들어줘야 합니다.
자세한 내용은 아래에서 더 설명할 예정입니다.
아무튼 이 쿼리를 생성할 때 kSecClass 라는 key에 대한 value로 5가지 class 중 하나를 지정해줍니다.
- 쿼리 딕셔너리에서 item class를 지정하기 위한 key
// item의 class를 value로 갖는 딕셔너리 Key
let kSecClass: CFString
- 쿼리 딕셔너리에서 item class의 value로 줄 수 있는 값 (kSecClass key의 value)
// generic password item을 가리키는 값
let kSecClassGenericPassword: CFString
// Internet password item을 가리키는 값
let kSecClassInternetPassword: CFString
// certificate item을 가리키는 값
let kSecClassCertificate: CFString
// cryptographic key item을 가리키는 값
let kSecClassKey: CFString
// identity item을 가리키는 값
let kSecClassIdentity: CFString
📌 Keychain Item attribute의 key-value
Keychain Item에는 저장할 데이터 뿐만 아니라 attribute들도 포함된다고 위에서 설명드렸습니다.
attribute를 통해 데이터를 나중에 검색할 수 있도록 하거나, 데이터가 사용·공유되는 방식 등을 제어할 수 있습니다.
💡 item class에 따라 적용 가능한 attribute가 다르기 때문에 사용하려는 class에 맞게 attribute를 구성할 수 있어야 합니다.
- kSecClassGenericPassword
- kSecClassInternetPassword
- kSecClassCertificate
- kSecClassIdentity
- kSecClassKey
Kechain CRUD 메서드
키체인에서 keychain item의 CRUD 작업 시에는 아래 4가지 메서드를 사용합니다.
// MARK: - Create: 키체인에 비밀정보 추가하기
func SecItemAdd(_ attributes: CFDictionary, _ result: UnsafeMutablePointer<CFTypeRef?>?) -> OSStatus
// MARK: - Read: 키체인에 저장된 Keychain item 찾기
func SecItemCopyMatching(_ query: CFDictionary, _ result: UnsafeMutablePointer<CFTypeRef?>?) -> OSStatus
// MARK: - Update: 키체인에 저장된 정보 변경하기
func SecItemUpdate(_ query: CFDictionary, _ attributesToUpdate: CFDictionary) -> OSStatus
// MARK: - Delete: 키체인에 저장된 정보 삭제하기
func SecItemDelete(_ query: CFDictionary) -> OSStatus
📌 결과 처리
위 메서드들은 OSStatus라는 타입으로 결과에 대한 값을 반환하는데, 해당 타입은 정수 형식으로 되어 있기 때문에 값을 그냥 출력하는 것으로는 어떤 에러인지 제대로 파악하기 힘듭니다.
print() 시에 아래 함수를 사용하면 유의미한 에러 메시지를 확인 가능합니다.
SecCopyErrorMessageString(status, nil)
Keychain을 통해 User Secrets 정보를 관리하는 예시
애플 공식 문서를 따라가며 실제로 Keychain을 어떻게 사용하여 유저의 민감한 정보를 관리할 수 있는지 확인해봅시다.
키체인 서비스는 암호화된 저장소로 쉽게 접근할 수 있도록 해주기 때문에 최소한의 상호작용으로 좋은 사용자 경험을 제공할 수 있습니다.
감이 잘 오지 않을 수도 있으니 사용 예시로 확인해보겠습니다.
아래 다이어그램은 인터넷 비밀번호를 Keychain Item으로 저장하는 경우의 플로우입니다.

공식 문서에서 키체인 사용 시 좋은 사용자 경험을 위해 3가지를 준수하라고 합니다.
- 필요할 때만 사용자를 참여시킬 것 (CREATE)
- 일반적인 상황에서 사용자를 괴롭히지 말 것 (READ)
- 변화를 깔쌈하게 처리할 것 (UPDATE, DELETE)
각각을 어떻게 준수해야 하는지 위 다이어그램의 흐름을 예시로 따라가며 설명하겠습니다.
📌 필요할 때만 사용자를 참여시킬 것
CREATE와 관련된 내용입니다.
- 처음에 앱이 credential(자격증명)을 필요로 할 때는 password가 키체인에 저장되어 있지 않은 상태이기 때문에 다이어그램의 오른쪽 흐름과 같이 앱이 사용자에게 인증을 요청합니다.
- 사용자가 인증 후 credential을 제공하면 앱은 SecItemAdd(_:_:) 함수를 호출하여 credential을 키체인에 저장하게 됩니다.
- 이후 앱은 정기적인 네트워크 접속을 계속하면서, 나중에 서버가 재인증을 요구하게 되는 순간 사용자 대신 키체인에서 credential을 검색하여 제공해줄 수 있습니다.

📌 일반적인 상황에서 사용자를 괴롭히지 말 것
READ와 관련된 내용입니다.
- 다이어그램 상에서 가장 일반적인 경로는 사용자 상호 작용이 필요하지 않은 중앙 부분입니다.
- 예를 들면 사용자가 잠시 자리를 비운 후 앱을 다시 시작하게 될 경우, 정기적인 재인증이 필요할 수 있습니다.
- 이런 경우 앱은 SecItemCopyMatching(_:_:) 함수를 사용하여 키체인에서 password를 검색하게 됩니다.
- 키체인에서 password가 발견되어 앱이 성공적으로 인증하게 되면, 사용자가 귀찮은 재인증 과정을 직접 수행할 필요 없이 계속 앱 사용이 가능해집니다.

📌 변화를 쌈@뽕(Gracefully...의역)하게 처리할 것
UPDATE와 관련된 내용입니다.
- 어떨 때는 사용자가 앱의 통제 범위 밖에서 credential을 변경하게 될 수 있습니다. ex) 앱과 동일한 서비스의 웹 인터페이스를 통한 비밀번호 변경·재설정
- 이런 상황이 발생하고 나면 앱이 keychain item을 검색했을 때, 이전 password를 가져오게 되고 인증에 실패하게 됩니다.
- 그러면 앱은 다이어그램의 오른쪽 흐름과 같이 사용자에게 새로운 인증을 요청하게 되고, 새 credential을 검증한 뒤에 SecItemUpdate(_:_:) 함수를 호출하여 키체인에 있는 기존 정보를 수정하게 됩니다.
DELETE와 관련된 내용입니다.
- 로그아웃과 같이 사용자가 네트워크 서비스와 완전히 연결을 끊기로 결정할 경우에, 앱은 로그아웃 처리와 함께 credential도 제거해줘야 합니다.
- 이 때 SecItemDelete(_:) 함수를 사용하여 키체인에서 password를 제거해줍니다.

예시 코드로 확인하기
이번에는 위에서 설명했던 흐름을 코드로 작성해보겠습니다.
📌 Keychain에 password item 추가(저장)하기
먼저 앱 상의 메모리에 저장할 Credential에 대한 구조체를 정의해줍니다.
간단하게 username과 password 프로퍼티를 넣어줬습니다.
Credentials의 내용들을 키체인에 저장하고, 키체인에서 읽어올 예정입니다.
struct Credentials {
var username: String
var password: String
}
키체인 접근 시 에러 처리를 위한 에러 타입을 정의해줍니다.
enum KeychainError: Error {
case noPassword
case unexpectedPasswordData
case unhandledError(status: OSStatus)
}
앱이 사용하는 서버의 주소를 static 상수로 정의해줍니다.
static let server = "www.example.com"
💡 서버 주소가 필요한 이유?
👉 keychain item을 저장할 때, account(계정)과 server(서버)에 대한 내용을 저장할 수 있는 attribute가 제공됩니다. 해당 attribute를 쿼리에 같이 작성해줌으로써 저장할 password에 대한 부수적인 정보들도 같이 넣어줄 수 있습니다.
이러한 attribute들을 잘 추가해서 나중에 키체인 아이템을 확실하게 검색할 수 있도록 구성해주어야 합니다.
자세한 내용은 아래 쿼리 작성 부분에서 설명드리겠습니다.
Add 쿼리 만들기
Credentials 구조체의 인스턴스와 위에서 정의해준 server 상수를 사용해서 add query를 만들 수 있습니다.
쿼리는 키체인 CRUD 함수에서 CFDictionary 형태로 변환되어야 하기 때문에 딕셔너리로 만들어줍니다.
위에서 설명한 것처럼 keychain item의 여러 attribute 중 Account와 Server에 대한 것들을 쿼리에 포함시켜줬습니다.
- kSecAttrAccount : 계정에 대한 정보로 Credentials의 username 값을 넣어줍니다. 이렇게 하면 username과 password를 나눠서 저장할 필요가 없어집니다. 하나의 keychain item에 username과 password가 모두 들어가게 되는 것이죠.
- kSecAttrServer : 해당 password가 관리되는 서버 정보를 명시하는데, 서버의 주소를 넣을 수 있습니다. 예시 코드에서는 실제 서버를 사용하지 않기 때문에 임의의 주소를 넣어줬습니다.
그리고 keychain item의 class를 지정하기 위한 attribute와, 저장하고자 하는 데이터에 대한 attribute를 추가해줍니다.
저장하는 데이터는 반드시 Data 타입이어야 하기 때문에 String 값을 Data로 인코딩해줍니다.
// KEY 역할
let account = credentials.username
// VALUE 역할 - data 타입이어야 한다.
let password = credentials.password.data(using: String.Encoding.utf8)!
// add 쿼리
let query: [String: Any] = [
// item class 종류
kSecClass as String: kSecClassInternetPassword,
// 저장하고자 하는 정보
kSecAttrAccount as String: account, // item의 계정 이름을 나타냄
kSecAttrServer as String: server, // item의 서버를 나타냄
// Data 인스턴스로 인코딩된 password
kSecValueData as String: password // item의 데이터
]
💡kSecClass로 kSecClassInternetPassword를 지정해줬는데, 이와 비슷한 kSecClassGenericPassword도 있습니다. generic password는 internet password와 거의 비슷하지만, kSecAttrServer 같이 원격 액세스와 관련된 특정 attribute가 없습니다. 해당 예시에서는 kSecAttrServer를 키체인 아이템 검색 시 식별자처럼 사용할 예정입니다. 그래서 internet password를 사용하게 되었습니다.
kSecAttrServer가 없어도, kSecAttrAccount에 저장을 위한 값이 아닌 식별자로 사용할 값을 넣어줘도 됩니다. 그렇게 할 경우네는 internet password 말고 generic password를 사용해도 됩니다.
나중에 포스팅할 수도 있지만 실제 진행 중인 프로젝트에서 저는 kSecClassGenericPassword class의 keychain item을 저장하면서 kSecAttrAccount는 저장할 데이터를 나타내는 key값으로 활용했습니다.
👉 ex) 파이어베이스 cloud function을 통한 원격 푸시 알림 사용 시 필요한 device token 값을 저장
var query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: "deviceToken",
kSecValueData as String: deviceTokenValue
]
Keychain item 추가(ADD)
작성된 add 쿼리를 SecItemAdd(_:_:) 함수에 넣어 호출해줍니다.
쿼리는 CFDictionary로 캐스팅해줘야 하고, 2번째 인자로 리턴 data를 참조할 수 있지만 무시해도 됩니다.
하지만 함수의 반환 결과는 반드시 확인해서 작업의 성공 여부에 따른 처리를 할 수 있어야 합니다. add 작업은 쿼리와 동일한 구성의 item이 이미 존재한다면 실패할 수 있습니다.
let status = SecItemAdd(query as CFDictionary, nil)
guard status == errSecSuccess else {
throw KeychainError.unhandledError(status: status)
}
📌 Keychain item 검색하기
위에서 작성한 것과 같이 쿼리 딕셔너리를 사용해 키체인 아이템을 검색할 수 있습니다.
쿼리 딕셔너리는 키체인 서비스에 어떤 item attribute를 찾아야 하고, item이 발견되면 무엇을 반환해야 하는지 알려줄 수 있어야 합니다.
그렇기 때문에 검색 쿼리 딕셔너리에는 검색 옵션을 매개변수로 지정해줄 수 있습니다. ex) 대소문자 구분 제어, 매칭되는 항목 수 제한 등...
Search 쿼리 만들기
add 쿼리 때와 비슷하게 작성할 수 있습니다.
하지만 검색 옵션을 위한 attribute들도 추가로 구성해줄 수 있습니다.
예시에서는 매칭되는 결과를 1개로 제한하는 옵션과, 검색된 item의 attribute와 data를 모두 받아오도록 설정해줬습니다.
let query: [String: Any] = [
kSecClass as String: kSecClassInternetPassword,
// 일종의 key의 역할 - server attribute가 매치되는 item을 찾게 됨
kSecAttrServer as String: server,
kSecMatchLimit as String: kSecMatchLimitOne, // result를 하나의 value로 한정짓는다.(원래 디폴트값임)
// attribute와 data 둘 다 요청
kSecReturnAttributes as String: true,
kSecReturnData as String: true
]
- kSecMatchLimit 검색 파라미터로 결과를 단일 값으로 제한해줍니다. 이렇게 하면 키체인에서 첫 번째로 매칭된 아이템만 받아오게 됩니다. (1개로 제한하지 않을 경우에는 배열 형식으로 반환)
- 이 쿼리는 위의 add 예시에서 password item을 추가할 때 사용한 server attribute와 매치되는 키체인 아이템을 찾게 됩니다. 위에서 설명했지만 server로 지정한 값을 검색 시 일종의 식별자로 사용하는 것이죠.
- 마지막으로 아이템의 attribute와 data를 모두 요청합니다. 왜냐하면 kSecAttrAccountattribute로 username 값도 저장했기 때문에 이를 불러오기 위함입니다. data에는 password를 저장했으니 당연히 요청해야겠죠.
Keychain item 검색(SEARCH)
작성된 search 쿼리를 SecItemCopyMatching(_:_:) 함수에 넣어 호출해줍니다.
- add 때와 마찬가지로, 반환된 status를 통해 검색 결과를 체크하여 에러를 핸들링해줍니다.
- 함수의 2번째 파라미터를 통해 반환되는 결과를 참조할 수 있습니다.
- 정상적으로 검색이 완료되면 결과 Data를 통해 원하는 값을 추출해줍니다.
var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &item)
guard status != errSecItemNotFound else {
throw KeychainError.noPassword
}
guard status == errSecSuccess else {
throw KeychainError.unhandledError(status: status)
}
// 검색 시 1개의 결과만 요청했으므로 단일 딕셔너리로 옴
guard let existingItem = item as? [String: Any],
let passwordData = existingItem[kSecValueData as String] as? Data,
let password = String(data: passwordData, encoding: .utf8),
let account = existingItem[kSecAttrAccount as String] as? String
else {
throw KeychainError.unexpectedPasswordData
}
let credentials = Credentials(username: account, password: password)
📌 Keychain item 업데이트/삭제 하기
keychain item을 업데이트 하려면, 먼저 검색을 해야 합니다.
그래서 키체인 아이템의 업데이트 작업은 암시적으로 검색을 먼저 수행하게 됩니다.
이를 위해 업데이트 시에는 search 쿼리와 update 쿼리, 총 2개의 쿼리가 필요합니다.
Search 쿼리 만들기
// 업데이트 작업 수행 시에 암시적으로 검색을 먼저 해야 하기 때문에 검색할때와 같은 형식으로 쿼리 생성
let query: [String: Any] = [
kSecClass as String: kSecClassInternetPassword,
kSecAttrServer as String: server
]
Update 쿼리 만들기
업데이트하고자 하는 내용들을 딕셔너리로 지정해줍니다. item의 data 뿐만 아니라 attribute도 업데이트 가능합니다.
// 업데이트할 내용에 대한 쿼리 생성
let account = newCredentials.username
let password = newCredentials.password.data(using: .utf8)!
let attributes: [String: Any] = [
kSecAttrAccount as String: account,
kSecValueData as String: password
]
Update 수행
검색 쿼리와 새로운 attributes를 사용해서 SecItemUpdate(_:_:) 함수를 호출해줍니다.
함수 호출이 성공하면 제공한 attribute에 따라 일치하는 모든 item을 수정하게 됩니다.
let status = SecItemUpdate(query as CFDictionary, attributes as CFDictionary)
guard status != errSecItemNotFound else {
throw KeychainError.noPassword
}
guard status == errSecSuccess else {
throw KeychainError.unhandledError(status: status)
}
Delete 수행
item을 삭제하는 것은 검색용 쿼리 딕셔너리 하나만 필요하다는 점을 제외하면 업데이트와 매우 비슷합니다.
Update에서 사용한 것과 동일한 쿼리를 사용해서 SecItemDelete(_:) 함수를 호출해줍니다.
업데이트와 비슷하게 키체인 서비스는 검색 파라미터와 일치하는 모든 키체인 항목을 삭제합니다.
// 삭제할 item을 검색하기 위한 쿼리
let query: [String: Any] = [
kSecClass as String: kSecClassInternetPassword,
kSecAttrServer as String: server
]
let status = SecItemDelete(query as CFDictionary)
guard status == errSecSuccess || status == errSecItemNotFound else {
throw KeychainError.unhandledError(status: status)
}
🚀 테스트 앱
https://github.com/kybeen/KeychainSample
GitHub - kybeen/KeychainSample: Keychain 공부용 샘플 앱
Keychain 공부용 샘플 앱. Contribute to kybeen/KeychainSample development by creating an account on GitHub.
github.com
아래처럼 간단하게 키체인의 CRUD 작업을 테스트할 수 있는 앱을 만들어봤습니다.
전체 코드는 좀 길기 때문에 깃허브 링크를 참고해주세요.

레퍼런스
https://developer.apple.com/documentation/foundation/userdefaults
https://developer.apple.com/documentation/security/keychain_services
'iOS' 카테고리의 다른 글
[ iOS ] String에서 숫자 찾기 & 패턴 찾기 (2) | 2024.05.30 |
---|---|
[ iOS ] Comparable 프로토콜을 사용하여 열거형 값을 비교하기 (0) | 2024.05.30 |
[iOS] SwiftUI의 TextField에 Clear Button 추가하기 (0) | 2024.05.28 |
[iOS] Build input file cannot be found: '~/Info.plist' 에러 발생 시 (0) | 2024.05.05 |
[iOS] Core Location 테스트하기 (외부 의존성의 응답을 테스트하기) (0) | 2024.05.03 |
Keychain
애플에서 사용자 데이터를 안전하게 저장할 수 있도록 제공하는 암호화된 데이터베이스
Keychain Service API를 사용하면 키체인 안에 사용자 데이터를 저장할 수 있습니다.
- 안전하게 저장해야 하는 작은 데이터를 저장할 때 사용 - ex) 비밀번호, 암호키, 인증키, 메모 등
- SandBox 밖에 데이터가 저장되기 때문에 앱을 삭제해도 데이터가 남아있음
- 디바이스가 Lock/Unlock되면 키체인도 같이 Lock/Unlock됨
- Secure Enclave라는 암호화 기능을 사용하는 하드웨어에서 키체인 데이터를 관리함
- Keychain Sharing을 통해 동일한 개발자에 속한 서로 다른 앱 간 키체인 공유가 가능

📌 UserDefaults와 비교
Keychain은 UserDefaults와 대체로 유사하지만 다른 점이 몇 가지 있습니다.
- UserDefaults로 저장되는 데이터는 plist에 저장되기 때문에 보안에 취약함
- UserDefaults에 저장된 데이터는 앱 삭제 시 같이 삭제됨
- UserDefaults는 앱이 시작될 때 plist 파일이 로드되어 싱글톤 형태로 접근하기 때문에, 데이터가 클 수록 로드 시 성능 저하가 발생
- UserDefaults는 App Group을 통해 Extension 간 데이터 공유가 가능
👉 그렇기 때문에 유저의 로그인 정보와 같이 민감한 정보들은 UserDefaults가 아닌 Keychain에 저장해주는 것이 좋습니다.
Keychain Item
키체인에 데이터를 저장할 때는 Keychain Item이라는 형태로 저장하게 됩니다.
즉, Keychain Item은 키체인에 저장하는 Data의 단위라고 할 수 있습니다.
- 비밀번호, 암호화 키 같은 정보를 키체인에 저장하고 싶을 때 Keychain Item으로 패키징
- Keychain Item으로 패키징하면 데이터 자체(data) 뿐만 아니라, item의 접근성을 제어하고 검색할 수 있도록 public하게 볼 수 있는 arribute들이 제공됨
- 키체인 서비스는 키체인에서 data, attribute 모두에 대해 데이터 암호화 및 저장을 처리
- 👉 나중에 승인된 프로세스는 키체인 서비스를 사용하여 item을 찾고, 데이터를 복호화할 수 있습니다.

📌 Keychain Item class의 key-value
Keychain item은 password(비밀번호), cryptographic key(암호화 키), certificate(인증서) 등과 같이 저장하려는 데이터의 종류에 따라 다양한 class로 제공됩니다. (Swift 문법적 의미의 class가 아님)
item이 어떤 class인지에 따라서 적용 가능한 attribute와, 시스템을 통한 데이터의 암호화 여부 등이 결정됩니다.
키체인에서 item에 대한 CRUD 작업이 이루어질 때는 딕셔너리 형태로 쿼리를 만들어줘야 합니다.
자세한 내용은 아래에서 더 설명할 예정입니다.
아무튼 이 쿼리를 생성할 때 kSecClass 라는 key에 대한 value로 5가지 class 중 하나를 지정해줍니다.
- 쿼리 딕셔너리에서 item class를 지정하기 위한 key
// item의 class를 value로 갖는 딕셔너리 Key
let kSecClass: CFString
- 쿼리 딕셔너리에서 item class의 value로 줄 수 있는 값 (kSecClass key의 value)
// generic password item을 가리키는 값
let kSecClassGenericPassword: CFString
// Internet password item을 가리키는 값
let kSecClassInternetPassword: CFString
// certificate item을 가리키는 값
let kSecClassCertificate: CFString
// cryptographic key item을 가리키는 값
let kSecClassKey: CFString
// identity item을 가리키는 값
let kSecClassIdentity: CFString
📌 Keychain Item attribute의 key-value
Keychain Item에는 저장할 데이터 뿐만 아니라 attribute들도 포함된다고 위에서 설명드렸습니다.
attribute를 통해 데이터를 나중에 검색할 수 있도록 하거나, 데이터가 사용·공유되는 방식 등을 제어할 수 있습니다.
💡 item class에 따라 적용 가능한 attribute가 다르기 때문에 사용하려는 class에 맞게 attribute를 구성할 수 있어야 합니다.
- kSecClassGenericPassword
- kSecClassInternetPassword
- kSecClassCertificate
- kSecClassIdentity
- kSecClassKey
Kechain CRUD 메서드
키체인에서 keychain item의 CRUD 작업 시에는 아래 4가지 메서드를 사용합니다.
// MARK: - Create: 키체인에 비밀정보 추가하기
func SecItemAdd(_ attributes: CFDictionary, _ result: UnsafeMutablePointer<CFTypeRef?>?) -> OSStatus
// MARK: - Read: 키체인에 저장된 Keychain item 찾기
func SecItemCopyMatching(_ query: CFDictionary, _ result: UnsafeMutablePointer<CFTypeRef?>?) -> OSStatus
// MARK: - Update: 키체인에 저장된 정보 변경하기
func SecItemUpdate(_ query: CFDictionary, _ attributesToUpdate: CFDictionary) -> OSStatus
// MARK: - Delete: 키체인에 저장된 정보 삭제하기
func SecItemDelete(_ query: CFDictionary) -> OSStatus
📌 결과 처리
위 메서드들은 OSStatus라는 타입으로 결과에 대한 값을 반환하는데, 해당 타입은 정수 형식으로 되어 있기 때문에 값을 그냥 출력하는 것으로는 어떤 에러인지 제대로 파악하기 힘듭니다.
print() 시에 아래 함수를 사용하면 유의미한 에러 메시지를 확인 가능합니다.
SecCopyErrorMessageString(status, nil)
Keychain을 통해 User Secrets 정보를 관리하는 예시
애플 공식 문서를 따라가며 실제로 Keychain을 어떻게 사용하여 유저의 민감한 정보를 관리할 수 있는지 확인해봅시다.
키체인 서비스는 암호화된 저장소로 쉽게 접근할 수 있도록 해주기 때문에 최소한의 상호작용으로 좋은 사용자 경험을 제공할 수 있습니다.
감이 잘 오지 않을 수도 있으니 사용 예시로 확인해보겠습니다.
아래 다이어그램은 인터넷 비밀번호를 Keychain Item으로 저장하는 경우의 플로우입니다.

공식 문서에서 키체인 사용 시 좋은 사용자 경험을 위해 3가지를 준수하라고 합니다.
- 필요할 때만 사용자를 참여시킬 것 (CREATE)
- 일반적인 상황에서 사용자를 괴롭히지 말 것 (READ)
- 변화를 깔쌈하게 처리할 것 (UPDATE, DELETE)
각각을 어떻게 준수해야 하는지 위 다이어그램의 흐름을 예시로 따라가며 설명하겠습니다.
📌 필요할 때만 사용자를 참여시킬 것
CREATE와 관련된 내용입니다.
- 처음에 앱이 credential(자격증명)을 필요로 할 때는 password가 키체인에 저장되어 있지 않은 상태이기 때문에 다이어그램의 오른쪽 흐름과 같이 앱이 사용자에게 인증을 요청합니다.
- 사용자가 인증 후 credential을 제공하면 앱은 SecItemAdd(_:_:) 함수를 호출하여 credential을 키체인에 저장하게 됩니다.
- 이후 앱은 정기적인 네트워크 접속을 계속하면서, 나중에 서버가 재인증을 요구하게 되는 순간 사용자 대신 키체인에서 credential을 검색하여 제공해줄 수 있습니다.

📌 일반적인 상황에서 사용자를 괴롭히지 말 것
READ와 관련된 내용입니다.
- 다이어그램 상에서 가장 일반적인 경로는 사용자 상호 작용이 필요하지 않은 중앙 부분입니다.
- 예를 들면 사용자가 잠시 자리를 비운 후 앱을 다시 시작하게 될 경우, 정기적인 재인증이 필요할 수 있습니다.
- 이런 경우 앱은 SecItemCopyMatching(_:_:) 함수를 사용하여 키체인에서 password를 검색하게 됩니다.
- 키체인에서 password가 발견되어 앱이 성공적으로 인증하게 되면, 사용자가 귀찮은 재인증 과정을 직접 수행할 필요 없이 계속 앱 사용이 가능해집니다.

📌 변화를 쌈@뽕(Gracefully...의역)하게 처리할 것
UPDATE와 관련된 내용입니다.
- 어떨 때는 사용자가 앱의 통제 범위 밖에서 credential을 변경하게 될 수 있습니다. ex) 앱과 동일한 서비스의 웹 인터페이스를 통한 비밀번호 변경·재설정
- 이런 상황이 발생하고 나면 앱이 keychain item을 검색했을 때, 이전 password를 가져오게 되고 인증에 실패하게 됩니다.
- 그러면 앱은 다이어그램의 오른쪽 흐름과 같이 사용자에게 새로운 인증을 요청하게 되고, 새 credential을 검증한 뒤에 SecItemUpdate(_:_:) 함수를 호출하여 키체인에 있는 기존 정보를 수정하게 됩니다.
DELETE와 관련된 내용입니다.
- 로그아웃과 같이 사용자가 네트워크 서비스와 완전히 연결을 끊기로 결정할 경우에, 앱은 로그아웃 처리와 함께 credential도 제거해줘야 합니다.
- 이 때 SecItemDelete(_:) 함수를 사용하여 키체인에서 password를 제거해줍니다.

예시 코드로 확인하기
이번에는 위에서 설명했던 흐름을 코드로 작성해보겠습니다.
📌 Keychain에 password item 추가(저장)하기
먼저 앱 상의 메모리에 저장할 Credential에 대한 구조체를 정의해줍니다.
간단하게 username과 password 프로퍼티를 넣어줬습니다.
Credentials의 내용들을 키체인에 저장하고, 키체인에서 읽어올 예정입니다.
struct Credentials {
var username: String
var password: String
}
키체인 접근 시 에러 처리를 위한 에러 타입을 정의해줍니다.
enum KeychainError: Error {
case noPassword
case unexpectedPasswordData
case unhandledError(status: OSStatus)
}
앱이 사용하는 서버의 주소를 static 상수로 정의해줍니다.
static let server = "www.example.com"
💡 서버 주소가 필요한 이유?
👉 keychain item을 저장할 때, account(계정)과 server(서버)에 대한 내용을 저장할 수 있는 attribute가 제공됩니다. 해당 attribute를 쿼리에 같이 작성해줌으로써 저장할 password에 대한 부수적인 정보들도 같이 넣어줄 수 있습니다.
이러한 attribute들을 잘 추가해서 나중에 키체인 아이템을 확실하게 검색할 수 있도록 구성해주어야 합니다.
자세한 내용은 아래 쿼리 작성 부분에서 설명드리겠습니다.
Add 쿼리 만들기
Credentials 구조체의 인스턴스와 위에서 정의해준 server 상수를 사용해서 add query를 만들 수 있습니다.
쿼리는 키체인 CRUD 함수에서 CFDictionary 형태로 변환되어야 하기 때문에 딕셔너리로 만들어줍니다.
위에서 설명한 것처럼 keychain item의 여러 attribute 중 Account와 Server에 대한 것들을 쿼리에 포함시켜줬습니다.
- kSecAttrAccount : 계정에 대한 정보로 Credentials의 username 값을 넣어줍니다. 이렇게 하면 username과 password를 나눠서 저장할 필요가 없어집니다. 하나의 keychain item에 username과 password가 모두 들어가게 되는 것이죠.
- kSecAttrServer : 해당 password가 관리되는 서버 정보를 명시하는데, 서버의 주소를 넣을 수 있습니다. 예시 코드에서는 실제 서버를 사용하지 않기 때문에 임의의 주소를 넣어줬습니다.
그리고 keychain item의 class를 지정하기 위한 attribute와, 저장하고자 하는 데이터에 대한 attribute를 추가해줍니다.
저장하는 데이터는 반드시 Data 타입이어야 하기 때문에 String 값을 Data로 인코딩해줍니다.
// KEY 역할
let account = credentials.username
// VALUE 역할 - data 타입이어야 한다.
let password = credentials.password.data(using: String.Encoding.utf8)!
// add 쿼리
let query: [String: Any] = [
// item class 종류
kSecClass as String: kSecClassInternetPassword,
// 저장하고자 하는 정보
kSecAttrAccount as String: account, // item의 계정 이름을 나타냄
kSecAttrServer as String: server, // item의 서버를 나타냄
// Data 인스턴스로 인코딩된 password
kSecValueData as String: password // item의 데이터
]
💡kSecClass로 kSecClassInternetPassword를 지정해줬는데, 이와 비슷한 kSecClassGenericPassword도 있습니다. generic password는 internet password와 거의 비슷하지만, kSecAttrServer 같이 원격 액세스와 관련된 특정 attribute가 없습니다. 해당 예시에서는 kSecAttrServer를 키체인 아이템 검색 시 식별자처럼 사용할 예정입니다. 그래서 internet password를 사용하게 되었습니다.
kSecAttrServer가 없어도, kSecAttrAccount에 저장을 위한 값이 아닌 식별자로 사용할 값을 넣어줘도 됩니다. 그렇게 할 경우네는 internet password 말고 generic password를 사용해도 됩니다.
나중에 포스팅할 수도 있지만 실제 진행 중인 프로젝트에서 저는 kSecClassGenericPassword class의 keychain item을 저장하면서 kSecAttrAccount는 저장할 데이터를 나타내는 key값으로 활용했습니다.
👉 ex) 파이어베이스 cloud function을 통한 원격 푸시 알림 사용 시 필요한 device token 값을 저장
var query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: "deviceToken",
kSecValueData as String: deviceTokenValue
]
Keychain item 추가(ADD)
작성된 add 쿼리를 SecItemAdd(_:_:) 함수에 넣어 호출해줍니다.
쿼리는 CFDictionary로 캐스팅해줘야 하고, 2번째 인자로 리턴 data를 참조할 수 있지만 무시해도 됩니다.
하지만 함수의 반환 결과는 반드시 확인해서 작업의 성공 여부에 따른 처리를 할 수 있어야 합니다. add 작업은 쿼리와 동일한 구성의 item이 이미 존재한다면 실패할 수 있습니다.
let status = SecItemAdd(query as CFDictionary, nil)
guard status == errSecSuccess else {
throw KeychainError.unhandledError(status: status)
}
📌 Keychain item 검색하기
위에서 작성한 것과 같이 쿼리 딕셔너리를 사용해 키체인 아이템을 검색할 수 있습니다.
쿼리 딕셔너리는 키체인 서비스에 어떤 item attribute를 찾아야 하고, item이 발견되면 무엇을 반환해야 하는지 알려줄 수 있어야 합니다.
그렇기 때문에 검색 쿼리 딕셔너리에는 검색 옵션을 매개변수로 지정해줄 수 있습니다. ex) 대소문자 구분 제어, 매칭되는 항목 수 제한 등...
Search 쿼리 만들기
add 쿼리 때와 비슷하게 작성할 수 있습니다.
하지만 검색 옵션을 위한 attribute들도 추가로 구성해줄 수 있습니다.
예시에서는 매칭되는 결과를 1개로 제한하는 옵션과, 검색된 item의 attribute와 data를 모두 받아오도록 설정해줬습니다.
let query: [String: Any] = [
kSecClass as String: kSecClassInternetPassword,
// 일종의 key의 역할 - server attribute가 매치되는 item을 찾게 됨
kSecAttrServer as String: server,
kSecMatchLimit as String: kSecMatchLimitOne, // result를 하나의 value로 한정짓는다.(원래 디폴트값임)
// attribute와 data 둘 다 요청
kSecReturnAttributes as String: true,
kSecReturnData as String: true
]
- kSecMatchLimit 검색 파라미터로 결과를 단일 값으로 제한해줍니다. 이렇게 하면 키체인에서 첫 번째로 매칭된 아이템만 받아오게 됩니다. (1개로 제한하지 않을 경우에는 배열 형식으로 반환)
- 이 쿼리는 위의 add 예시에서 password item을 추가할 때 사용한 server attribute와 매치되는 키체인 아이템을 찾게 됩니다. 위에서 설명했지만 server로 지정한 값을 검색 시 일종의 식별자로 사용하는 것이죠.
- 마지막으로 아이템의 attribute와 data를 모두 요청합니다. 왜냐하면 kSecAttrAccountattribute로 username 값도 저장했기 때문에 이를 불러오기 위함입니다. data에는 password를 저장했으니 당연히 요청해야겠죠.
Keychain item 검색(SEARCH)
작성된 search 쿼리를 SecItemCopyMatching(_:_:) 함수에 넣어 호출해줍니다.
- add 때와 마찬가지로, 반환된 status를 통해 검색 결과를 체크하여 에러를 핸들링해줍니다.
- 함수의 2번째 파라미터를 통해 반환되는 결과를 참조할 수 있습니다.
- 정상적으로 검색이 완료되면 결과 Data를 통해 원하는 값을 추출해줍니다.
var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &item)
guard status != errSecItemNotFound else {
throw KeychainError.noPassword
}
guard status == errSecSuccess else {
throw KeychainError.unhandledError(status: status)
}
// 검색 시 1개의 결과만 요청했으므로 단일 딕셔너리로 옴
guard let existingItem = item as? [String: Any],
let passwordData = existingItem[kSecValueData as String] as? Data,
let password = String(data: passwordData, encoding: .utf8),
let account = existingItem[kSecAttrAccount as String] as? String
else {
throw KeychainError.unexpectedPasswordData
}
let credentials = Credentials(username: account, password: password)
📌 Keychain item 업데이트/삭제 하기
keychain item을 업데이트 하려면, 먼저 검색을 해야 합니다.
그래서 키체인 아이템의 업데이트 작업은 암시적으로 검색을 먼저 수행하게 됩니다.
이를 위해 업데이트 시에는 search 쿼리와 update 쿼리, 총 2개의 쿼리가 필요합니다.
Search 쿼리 만들기
// 업데이트 작업 수행 시에 암시적으로 검색을 먼저 해야 하기 때문에 검색할때와 같은 형식으로 쿼리 생성
let query: [String: Any] = [
kSecClass as String: kSecClassInternetPassword,
kSecAttrServer as String: server
]
Update 쿼리 만들기
업데이트하고자 하는 내용들을 딕셔너리로 지정해줍니다. item의 data 뿐만 아니라 attribute도 업데이트 가능합니다.
// 업데이트할 내용에 대한 쿼리 생성
let account = newCredentials.username
let password = newCredentials.password.data(using: .utf8)!
let attributes: [String: Any] = [
kSecAttrAccount as String: account,
kSecValueData as String: password
]
Update 수행
검색 쿼리와 새로운 attributes를 사용해서 SecItemUpdate(_:_:) 함수를 호출해줍니다.
함수 호출이 성공하면 제공한 attribute에 따라 일치하는 모든 item을 수정하게 됩니다.
let status = SecItemUpdate(query as CFDictionary, attributes as CFDictionary)
guard status != errSecItemNotFound else {
throw KeychainError.noPassword
}
guard status == errSecSuccess else {
throw KeychainError.unhandledError(status: status)
}
Delete 수행
item을 삭제하는 것은 검색용 쿼리 딕셔너리 하나만 필요하다는 점을 제외하면 업데이트와 매우 비슷합니다.
Update에서 사용한 것과 동일한 쿼리를 사용해서 SecItemDelete(_:) 함수를 호출해줍니다.
업데이트와 비슷하게 키체인 서비스는 검색 파라미터와 일치하는 모든 키체인 항목을 삭제합니다.
// 삭제할 item을 검색하기 위한 쿼리
let query: [String: Any] = [
kSecClass as String: kSecClassInternetPassword,
kSecAttrServer as String: server
]
let status = SecItemDelete(query as CFDictionary)
guard status == errSecSuccess || status == errSecItemNotFound else {
throw KeychainError.unhandledError(status: status)
}
🚀 테스트 앱
https://github.com/kybeen/KeychainSample
GitHub - kybeen/KeychainSample: Keychain 공부용 샘플 앱
Keychain 공부용 샘플 앱. Contribute to kybeen/KeychainSample development by creating an account on GitHub.
github.com
아래처럼 간단하게 키체인의 CRUD 작업을 테스트할 수 있는 앱을 만들어봤습니다.
전체 코드는 좀 길기 때문에 깃허브 링크를 참고해주세요.

레퍼런스
https://developer.apple.com/documentation/foundation/userdefaults
https://developer.apple.com/documentation/security/keychain_services
'iOS' 카테고리의 다른 글
[ iOS ] String에서 숫자 찾기 & 패턴 찾기 (2) | 2024.05.30 |
---|---|
[ iOS ] Comparable 프로토콜을 사용하여 열거형 값을 비교하기 (0) | 2024.05.30 |
[iOS] SwiftUI의 TextField에 Clear Button 추가하기 (0) | 2024.05.28 |
[iOS] Build input file cannot be found: '~/Info.plist' 에러 발생 시 (0) | 2024.05.05 |
[iOS] Core Location 테스트하기 (외부 의존성의 응답을 테스트하기) (0) | 2024.05.03 |