Keychain은 비밀번호, 암호화 키, 인증서 및 기밀 메모를 안전하게 저장하도록 설계된 iOS의 보안 저장소입니다. Apple Security Documentation (2025)에 따르면, Keychain은 A7 칩 이상의 모든 기기에서 Secure Enclave를 통한 하드웨어 암호화를 사용합니다. iOS Keychain의 아키텍처를 이해하는 것은 모든 개발자가 애플리케이션 토큰과 시크릿을 올바르게 저장하기 위해 필수적입니다.
핵심 요점
iOS Keychain은 Apple 운영 체제에 내장된 기밀 데이터 저장을 위한 보안 메커니즘입니다. UserDefaults나 일반 파일과 달리 Keychain은 모든 항목을 하드웨어 수준에서 암호화하고 보안 정책에 기반한 세분화된 액세스 제어를 제공합니다.
Keychain은 iOS 2.0에서 도입되었으며 이후 상당한 변화를 겪었습니다: iOS 7에서는 Secure Enclave를 통한 하드웨어 키 지원이 추가되었고, iOS 9에서는 Access Groups를 통한 앱 간 Keychain 공유가 도입되었으며, iOS 13에서는 LAContext를 통한 생체 인증 바인딩 지원이 추가되었습니다. Apple WWDC Session(2024)에 따르면, App Store 상위 100개 iOS 앱의 90% 이상이 인증 토큰 저장에 Keychain을 사용합니다.
아키텍처적으로 Keychain은 애플리케이션 샌드박스 외부에 위치한 암호화된 SQLite 데이터베이스입니다. 각 항목(SecItem)은 개별 키로 암호화되며, 이 키는 다시 Secure Enclave 하드웨어 키로 보호됩니다. 시스템 서비스 Securityd는 애플리케이션 entitlements 및 요청된 보호 클래스에 따라 Keychain에 대한 액세스를 관리합니다.
다른 저장 방법에 비해 Keychain의 중요한 장점: 데이터는 OS에 의해 자동으로 암호화 및 복호화됩니다. 개발자는 수동으로 암호화를 구현할 필요가 없습니다 - 올바른 매개변수로 SecItemAdd를 호출하기만 하면 됩니다. iOS는 Access Groups가 올바르게 구성된 경우 Keychain의 데이터를 다른 애플리케이션이 읽을 수 없음을 보장합니다.
Keychain 아키텍처는 여러 수준으로 구성됩니다: 물리적(Secure Enclave), 시스템(Security.framework), 애플리케이션(SecItem* API) 및 논리적(Access Groups, Protection Classes). 각 수준을 이해하면 시크릿 저장소를 올바르게 설계하는 데 도움이 됩니다.
Keychain 작업을 위한 주요 API는 Security framework 함수입니다: 추가를 위한 SecItemAdd, 읽기를 위한 SecItemCopyMatching, 업데이트를 위한 SecItemUpdate, 삭제를 위한 SecItemDelete. 각 함수는 찾거나 저장할 항목의 속성을 설명하는 쿼리 사전을 받습니다.
주요 쿼리 속성: kSecClass - 항목 유형(kSecClassGenericPassword, kSecClassKey, kSecClassCertificate), kSecAttrAccount - 클래스 내 고유 식별자, kSecValueData - 저장된 데이터(Data), kSecAttrAccessible - 보호 클래스. SecItemCopyMatching은 kSecReturnData 플래그와 함께 항목 데이터를 반환하고, kSecMatchLimit과 함께 결과 수를 반환합니다.
중요: 모든 함수는 OSStatus를 반환합니다. 성공적인 작업은 errSecSuccess(0)를 반환합니다. 오류: errSecItemNotFound(-25300) - 항목을 찾을 수 없음, errSecDuplicateItem(-25299) - 항목이 이미 존재함, errSecAuthFailed(-25293) - 생체 인증 실패. 개발자는 각 상태를 올바르게 처리해야 합니다.
Protection Class는 Keychain의 데이터를 읽을 수 있는 시기를 결정하는 kSecAttrAccessible 속성입니다. iOS는 다양한 가용성 및 보안 수준을 가진 6개의 보호 클래스를 지원합니다.
대부분의 시나리오에서 권장되는 클래스는 kSecAttrAccessibleWhenUnlockedThisDeviceOnly입니다: 데이터는 기기 잠금이 해제된 경우에만 사용 가능하며 iCloud Backup에 복사되지 않습니다. 재부팅 후 사용 가능해야 하는 데이터(첫 잠금 해제 후에만)에는 kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly를 사용하세요. 매 액세스마다 생체 인증이 필요한 중요한 데이터의 경우 kSecAttrAccessibleWhenUnlockedThisDeviceOnly를 생체 인증을 요구하는 ACL과 결합하세요.
ThisDeviceOnly 접미사가 없는 클래스(kSecAttrAccessibleWhenUnlocked, kSecAttrAccessibleAfterFirstUnlock)는 iCloud Backup에 복사를 허용합니다. 이는 사용자에게 편리하지만 보안을 감소시킵니다 - 백업에서 데이터를 복원할 수 있습니다. 인증 토큰의 경우 항상 ThisDeviceOnly를 사용하세요.
Access Control List(ACL)는 사용자 인증에 따라 Keychain 항목에 대한 작업을 제한하는 메커니즘입니다. ACL은 SecAccessControlCreateWithFlags를 통해 설정되며 항목 저장 시 kSecAttrAccessControl 속성에 전달됩니다.
지원되는 플래그: kSecAccessControlUserPresence - 모든 인증(Face ID, Touch ID 또는 암호), kSecAccessControlBiometryCurrentSet - 생체 인증만(현재 등록된 지문 또는 얼굴), kSecAccessControlDevicePasscode - 암호만. ACL은 모든 작업에 적용됩니다: 항목 읽기, 업데이트 및 삭제에도 인증이 필요합니다.
iOS 15+에서는 Apple Watch용 플래그 kSecAccessControlWatch가 등장했습니다 - 페어링된 시계를 통한 인증을 허용합니다. ACL을 결합할 수 있습니다: 예를 들어 kSecAccessControlUserPresence 또는 kSecAccessControlBiometryAny와 선택적 암호(.or 방향).
iOS Keychain은 4가지 주요 항목 클래스(kSecClass)를 지원하며, 각각 고유한 데이터 유형에 맞게 설계되었습니다. 올바른 클래스를 선택하면 구성 및 항목 검색이 간소화됩니다.
kSecClassGenericPassword - 일반 비밀번호: 가장 일반적으로 사용되는 클래스. 고유 키(kSecAttrAccount)를 사용하여 임의의 이진 데이터(Data)를 저장합니다. 토큰, API 키, PIN 코드에 적합합니다. 사용에 추가 entitlements가 필요하지 않습니다.
kSecClassInternetPassword - 인터넷 비밀번호: 네트워크 리소스와 관련된 데이터를 저장합니다. 추가 속성: kSecAttrServer(서버 도메인), kSecAttrProtocol(https, ftp), kSecAttrPort, kSecAttrAuthenticationType. iOS는 AutoFill을 통해 이러한 비밀번호를 자동으로 채울 수 있습니다.
kSecClassKey - 암호화 키: 암호화 키(AES, RSA, EC) 저장용. 키는 Data가 아닌 SecKeyRef로 저장됩니다. kSecClassCertificate - 디지털 인증서 저장 및 확인용 X.509 인증서. 두 클래스 모두 암호화 작업 이해와 올바른 속성 구성이 필요합니다.
실제로 모바일 앱에서 Keychain 사용의 95%는 인증 토큰 저장용 kSecClassGenericPassword와 개인 암호화 키 저장용 kSecClassKey로 처리됩니다. kSecClassCertificate는 드물게 사용되며, 일반적으로 자체 PKI가 있는 엔터프라이즈 앱에서 사용됩니다.
Security framework를 사용하여 Swift에서 Keychain으로 작업하는 실제 예제를 살펴보겠습니다. 각 예제에는 오류 처리와 올바른 Protection Class 구성이 포함되어 있습니다.
기본 예제는 WhenUnlockedThisDeviceOnly 보호와 함께 Keychain에 인증 토큰을 저장합니다. 키(kSecAttrAccount)는 서비스 식별자이고, 데이터(kSecValueData)는 Data 형식의 토큰입니다.
import Security
enum KeychainError: Error {
case unexpectedStatus(OSStatus)
}
func saveToken(token: String, service: String) throws {
let data = Data(token.utf8)
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: "auth_token",
kSecValueData as String: data,
kSecAttrAccessible as String:
kSecAttrAccessibleWhenUnlockedThisDeviceOnly
]
SecItemDelete(query as CFDictionary)
let status = SecItemAdd(query as CFDictionary, nil)
guard status == errSecSuccess else {
throw KeychainError.unexpectedStatus(status)
}
}
func readToken(service: String) throws -> String {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: "auth_token",
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne
]
var result: AnyObject?
let status = SecItemCopyMatching(
query as CFDictionary, &result
)
guard status == errSecSuccess,
let data = result as? Data else {
throw KeychainError.unexpectedStatus(status)
}
return String(decoding: data, as: UTF8.self)
}
예제는 SecAccessControlCreateWithFlags를 사용하여 키를 생체 인증에 바인딩하는 방법을 보여줍니다. 항목에 대한 각 액세스에는 Face ID 또는 Touch ID가 필요합니다.
import LocalAuthentication
func saveWithBiometry(data: Data, key: String) throws {
let accessControl = SecAccessControlCreateWithFlags(
nil,
kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
.biometryCurrentSet,
nil
)
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecValueData as String: data,
kSecAttrAccessControl as String: accessControl as Any
]
SecItemDelete(query as CFDictionary)
let status = SecItemAdd(query as CFDictionary, nil)
guard status == errSecSuccess else {
throw KeychainError.unexpectedStatus(status)
}
}
예제는 동일한 개발자의 앱 간에 공유 Keychain 액세스를 위한 Access Group 구성 방법을 보여줍니다. keychain-access-groups entitlement가 필요합니다.
// Capabilities: Keychain Sharing 활성화됨
// App IDs: group.com.example.shared
func saveSharedToken(token: Data) {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: "shared_token",
kSecValueData as String: token,
kSecAttrAccessGroup as String:
"group.com.example.shared",
kSecAttrAccessible as String:
kSecAttrAccessibleWhenUnlockedThisDeviceOnly
]
SecItemAdd(query as CFDictionary, nil)
}
iOS Keychain을 올바르게 사용하려면 일반적인 취약점과 데이터 손실을 방지하는 몇 가지 핵심 규칙을 따라야 합니다.
모든 인증 시크릿에 ThisDeviceOnly를 사용하세요: kSecAttrAccessibleWhenUnlockedThisDeviceOnly는 토큰이 iCloud Backup에 포함되지 않도록 보장합니다. 공격자가 백업에 액세스하더라도 이 플래그가 있는 Keychain 데이터는 사용할 수 없습니다. 예외는 모든 사용자 기기에서 사용 가능해야 하는 데이터(예: 독점 서비스용 암호화 키)로, kSecAttrAccessibleWhenUnlocked를 kSecAttrSynchronizable과 함께 사용하세요.
원시 비밀번호를 저장하지 마세요 - 해시 또는 세션 토큰을 저장하세요. Apple Security Guide(2025)는 Keychain에 사용자 비밀번호를 평문으로 절대 저장하지 말 것을 권장합니다. 대신 OAuth 2.0을 통한 성공적인 인증 후 서버에서 받은 리프레시 토큰을 저장하세요. 비밀번호는 토큰을 얻는 데만 사용되며 즉시 메모리에서 제거됩니다.
Keychain 오류를 올바르게 처리하세요: 각 Keychain 작업은 확인해야 하는 OSStatus를 반환합니다. 특히 errSecItemNotFound(토큰 만료 또는 삭제)와 errSecAuthFailed(생체 인증 실패)에 주의하세요. 첫 번째 경우 앱이 새 인증을 요청해야 합니다. 두 번째 경우 사용자에게 대체 방법(암호)을 표시하세요. 절대 errSecItemNotFound 상태를 무시하지 마세요 - nil을 읽으려고 시도하면 앱이 충돌합니다.
실제 기기에서 Keychain을 테스트하세요: 시뮬레이터에는 Secure Enclave가 없으며 생체 인식 ACL을 지원하지 않습니다. 항상 시나리오를 확인하세요: 첫 실행, 백업 복원, 기기 암호 변경, 앱 삭제 및 재설치. 실제 기기에서 앱이 삭제되어도 Keychain은 유지되지만, kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly 플래그를 사용하지 않은 경우에 한하며, 이 플래그는 암호가 제거될 때 지워집니다.
Keychain 작업 수를 최소화하세요: 각 읽기 또는 쓰기 작업은 스레드를 차단할 수 있는 시스템 서비스 Securityd를 호출합니다. 세션 기간 동안 읽은 토큰을 메모리에 캐시하고 앱 재시작 또는 인증 오류(서버 401) 시에만 Keychain에 다시 액세스하세요. iOS는 기기가 잠기면 자동으로 Keychain을 잠그므로 생체 인식 요청과 함께 LAContext를 통한 읽기를 계획하세요.
자주 묻는 질문
네, 보호 클래스 kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly 또는 kSecAttrAccessibleAfterFirstUnlock을 사용하세요. 재부팅 후 첫 기기 잠금 해제 이후 데이터를 사용할 수 있습니다. 앱 실행 시 자동 액세스(잠금 해제 대기 없음)를 위해서는 kSecAttrAccessibleAlways를 사용하지만 보안이 감소합니다.
각 데이터 유형에 대한 kSecClass가 포함된 쿼리로 SecItemDelete를 호출하세요. 모든 앱 항목을 완전히 지우려면 다음을 실행하세요: SecItemDelete([kSecClass as String: kSecClassGenericPassword] as CFDictionary). kSecClassKey, kSecClassCertificate 및 kSecClassInternetPassword에 대해 반복하세요.
kSecAttrAccessible은 데이터를 사용할 수 있는 시기를 결정합니다(잠금 해제 시, 첫 잠금 해제 후 등). kSecAttrAccessControl은 누가 액세스할 수 있는지 결정합니다(생체 인증, 암호, 모든 인증). 이들은 결합됩니다: 먼저 Protection Class, 그 다음 ACL. 예를 들어, 데이터는 잠금 해제 시에만 그리고 Face ID 후에만 사용할 수 있습니다.
이유: 항목이 저장된 적이 없음, 암호 제거 시 항목이 삭제됨(kSecAttrAccessibleWhenPasscodeSet 사용 시), 앱 재설치(Keychain은 유지되지만 새 기기에서 백업에서 복원되지 않음), Access Group 또는 개발자 팀 식별자 변경. kSecAttrService 및 kSecAttrAccount를 확인하세요.
LocalAuthentication의 LAContext를 사용하세요: context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil)을 호출하세요. true를 반환하면 기기가 Touch ID 또는 Face ID를 지원합니다. Keychain ACL의 경우 biometryCurrentSet 플래그(현재 생체 데이터만) 또는 biometryAny(이전에 등록된 모든 것)를 사용하세요.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.