키체인
고친 사람 github-actions[bot]
키체인은 앱이 비밀번호나 로그인 토큰 같은 비밀을 운영체제에 맡겨 두게 해 줍니다. 애플 기기에 처음부터 들어 있는 보관함입니다. 맡긴 비밀은 운영체제가 암호화해 지킵니다. 돌려줄 때는 맡긴 앱에게만 돌려줍니다.
쉽고 빠른 이해
키체인은 애플 기기에 딸린 비밀 보관함입니다. 아이폰 앱이 로그인하고 받은 토큰을 여기에 넣어 두면, 앱을 껐다 켜도 로그인한 채로 시작합니다.
토큰을 앱 폴더의 파일에 그냥 적어 두면 그 파일을 손에 넣은 사람은 누구나 읽습니다. 파일을 암호화하려 해도 그 암호 키를 또 어딘가에 둬야 합니다. 키를 지키려고 또 키를 쓰면 이 문제는 끝없이 이어집니다. 키체인은 그 암호 키를 앱이 아니라 운영체제와 기기 하드웨어가 쥐게 해서 이 문제를 끝냅니다.
어떻게 도나:
- 앱이 「이 이름표로 이 값을 맡아 달라」고 운영체제에 부탁합니다
- 운영체제가 값을 암호화해 앱 밖의 저장소에 넣습니다
- 같은 앱이 이름표로 찾으면, 운영체제가 기기가 잠겼는지와 부른 앱이 누구인지 확인하고 돌려줍니다
대가가 있습니다. 작은 비밀만 담는 곳입니다. 부르는 코드도 번거롭습니다. 잠긴 기기에서는 못 꺼내는 값이 있습니다. 앱을 지워도 항목이 남을 수 있어서 그 뒷정리는 앱이 맡습니다.
상세
이 절은 키체인이 어떤 물건인지와 비밀을 왜 앱 밖에 맡기는지부터 봅니다. 그다음 항목이 들어가는 곳과 모양, 넣고 꺼내는 함수를 봅니다. 마지막으로 언제 어느 앱이 꺼낼 수 있는지, 앱을 지운 뒤 남는 항목, 키체인이 맡지 않는 일을 차례로 봅니다.
아이폰 앱이 로그인한 사용자의 리프레시 토큰을 보관하는 경우를 줄곧 예로 씁니다. 리프레시 토큰은 로그인을 다시 하지 않고도 서버에 내밀 새 토큰을 받아 오게 해 주는 긴 수명의 토큰입니다. 이 토큰을 키체인에 넣어 두면 앱을 껐다 켜도 로그인한 채로 시작합니다.
운영체제에 딸린 보관함
키체인은 애플이 iOS와 macOS에 넣어 둔 비밀 보관 기능입니다. 비밀번호, 로그인 토큰, 인증서, 암호 키처럼 작고 새면 곤란한 값을 담습니다. 따로 설치하는 프로그램이 아니라 운영체제와 함께 배포됩니다.
맥을 쓰는 백엔드 개발자라면 이미 키체인을 써 본 셈입니다. 맥에서 Git이 원격 저장소 비밀번호를 한 번 묻고 다음부터는 묻지 않는 경우가 많습니다. 그 비밀번호가 키체인에 들어가 있습니다. 맥에는 키체인 안을 들여다보는 「키체인 접근」이라는 앱도 있습니다.
비밀을 앱 밖에 맡기는 이유
앱이 토큰을 보관하는 가장 쉬운 방법은 앱 폴더 안의 파일에 적는 것입니다. iOS 앱에서는 앱 설정을 담는 UserDefaults가 그런 파일입니다. 적은 값은 암호화되지 않은 글자로 들어 있습니다. 기기 백업에도 함께 실립니다. 백업이나 기기를 손에 넣은 사람이 꺼내 읽을 수 있습니다.
그러면 앱이 직접 암호화하면 될 것 같습니다. 그런데 암호화에 쓴 키를 다시 어딘가에 둬야 합니다. 그 키를 같은 앱 폴더에 두면 자물쇠 바로 옆에 열쇠를 걸어 둔 꼴이 됩니다.
키체인은 그 암호 키를 앱이 아니라 운영체제와 기기 하드웨어가 쥐게 합니다. 앱은 키를 한 번도 만지지 않고 「맡아 달라」와 「돌려 달라」만 말합니다. 비밀이 앱 폴더에 없으니, 앱 폴더의 파일이 전부 새어 나가도 그 안에서 토큰은 나오지 않습니다.
앱 밖의 저장소
iOS 앱은 저마다 샌드박스 안에서 돕니다. 샌드박스는 앱이 자기 폴더 밖을 못 건드리게 가두는 울타리입니다. 키체인 항목은 어느 앱의 샌드박스에도 들지 않고, 운영체제가 관리하는 암호화된 데이터베이스에 들어갑니다.
앱은 이 데이터베이스 파일을 직접 열 수 없습니다. 앱이 부탁하면 운영체제가 대신 읽고 써 줍니다. 그때 운영체제는 부른 앱에게 그 앱의 항목만 보여 줍니다.
flowchart TD
subgraph SA["앱 A 의 샌드박스"]
A["앱 A"]
end
subgraph SB["앱 B 의 샌드박스"]
B["앱 B"]
end
A -->|"맡아 달라 · 돌려 달라"| OS["운영체제"]
B -->|"맡아 달라 · 돌려 달라"| OS
OS --> DB["암호화된 키체인 데이터베이스"]
OS --> HW["기기 하드웨어가 쥔 키"]
HW -->|"푼다"| DB
그림에서 앱에서 데이터베이스로 바로 가는 화살표는 없습니다. 모든 요청이 운영체제를 거칩니다. 데이터베이스를 푸는 키도 운영체제와 기기 하드웨어 쪽에만 있습니다.
항목과 속성
키체인에 넣는 하나하나를 항목이라고 부릅니다. 항목은 비밀 값 하나와, 그 값을 찾을 때 쓰는 속성 몇 개로 이루어집니다. 속성은 이름표입니다. 이름표에는 「어느 서비스의 어느 계정 것인가」를 적습니다. 꺼낼 때 그 이름표로 찾습니다.
리프레시 토큰이라면 서비스 속성에 api.example.com, 계정 속성에 refresh_token 을 적고
비밀 값에 토큰을 넣습니다. 나중에 같은 서비스와 계정으로 찾으면 그 토큰이 나옵니다.
항목은 다섯 종류로 나뉩니다. 종류마다 붙일 수 있는 속성이 다릅니다. 아래 표의 개인 키는 공개 키 암호에서 주인만 쥐는 쪽 키를 말합니다.
| 종류 | 담는 것 | 이름 |
|---|---|---|
| 일반 비밀번호 | 앱이 정한 비밀. 로그인 토큰도 여기에 넣는다 | kSecClassGenericPassword |
| 인터넷 비밀번호 | 웹사이트나 서버 계정의 비밀번호 | kSecClassInternetPassword |
| 인증서 | 인증서 | kSecClassCertificate |
| 키 | 암호 키 | kSecClassKey |
| 아이덴티티 | 인증서와 그 짝인 개인 키 한 벌 | kSecClassIdentity |
로그인 토큰은 대개 일반 비밀번호 종류로 넣습니다. 이름과 달리 비밀번호가 아닌 값도 담는 종류입니다.
넣고 꺼내는 네 함수
앱은 키체인 서비스(Keychain Services)라는 API(Application Programming Interface, 응용 프로그램 인터페이스)로 항목을 다룹니다. 하는 일마다 함수가 하나씩 있습니다.
| 하는 일 | 함수 |
|---|---|
| 넣기 | SecItemAdd |
| 찾기 | SecItemCopyMatching |
| 고치기 | SecItemUpdate |
| 지우기 | SecItemDelete |
네 함수 모두 속성을 담은 사전 하나를 받습니다. 사전은 이름과 값의 짝을 모은 묶음입니다. 결과는 성공인지 무엇이 틀렸는지를 알리는 상태 코드로 돌려줍니다.
아래는 Swift로 리프레시 토큰을 넣는 코드입니다. token 은 토큰 문자열을 바이트로 바꾼
값입니다. 같은 항목을 일부러 두 번 넣어 봅니다.
let item: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: "api.example.com",
kSecAttrAccount as String: "refresh_token",
kSecValueData as String: token,
]
let q = item as CFDictionary
SecItemAdd(q, nil) // errSecSuccess
SecItemAdd(q, nil) // errSecDuplicateItem
첫 호출은 성공을 뜻하는 errSecSuccess 를 돌려줍니다. 둘째 호출은 같은 서비스와 계정의 항목이
이미 있다는 errSecDuplicateItem 을 돌려줍니다. 넣기 함수는 있는 항목을 덮어쓰지 않습니다.
그래서 토큰을 새것으로 바꿀 때는 고치기 함수를 먼저 불러 봅니다. 항목이 없다는 답이 오면 그때 넣기 함수를 부릅니다.
잠긴 기기에서 꺼낼 수 있는 때
항목을 넣을 때 「언제 꺼낼 수 있나」를 함께 정합니다. 항목을 푸는 키가 기기의 잠금 상태에
따라 쓸 수 있게 되거나 못 쓰게 되기 때문입니다. 이 속성의 이름은 kSecAttrAccessible 입니다.
잠금 상태와 키가 엮이는 까닭은 키를 만드는 재료에 있습니다. 항목을 푸는 키는 사용자의 기기 암호를 재료로 삼아 만들어집니다. 그래서 사용자가 암호를 넣기 전에는 운영체제도 그 키를 쓸 수 없습니다.
기기는 아래처럼 상태를 옮겨 다닙니다. 켠 직후에는 사용자가 아직 기기 암호를 한 번도 넣지 않은 상태입니다.
stateDiagram-v2
state "켠 뒤 아직 안 푼 상태" as S0
state "잠금이 풀린 상태" as S1
state "다시 잠긴 상태" as S2
[*] --> S0: 기기를 켠다
S0 --> S1: 기기 암호를 넣는다
S1 --> S2: 화면을 잠근다
S2 --> S1: 다시 푼다
어느 상태에서 꺼낼 수 있는지를 설정값으로 고릅니다. 자주 쓰는 셋은 아래 표와 같습니다. 셋째
이름 끝의 ThisDeviceOnly 는 그 항목이 이 기기 밖으로 나가지 않는다는 뜻입니다.
| 설정값 | 꺼낼 수 있는 때 |
|---|---|
kSecAttrAccessibleWhenUnlocked |
잠금이 풀린 상태에서만 |
kSecAttrAccessibleAfterFirstUnlock |
켠 뒤 한 번 풀고 나면, 다시 잠긴 상태에서도 |
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly |
기기 암호가 걸려 있고 잠금이 풀린 상태에서만. 기기 암호를 없애면 항목이 지워진다 |
백엔드와 맞닿는 것은 첫째 설정값입니다. 서버가 푸시 알림으로 앱을 뒤에서 깨웠는데 기기가 잠겨
있으면, 첫째 설정값으로 넣은 토큰은 꺼내지지 않습니다. 찾기 함수가
errSecInteractionNotAllowed 를 돌려줍니다. 앱은 토큰이 없어 서버를 못 부릅니다. 뒤에서 도는
작업이 쓰는 토큰은 둘째 설정값으로 넣습니다.
키체인 항목은 설정에 따라 같은 애플 계정의 다른 기기로 동기화될 수 있습니다(아래 「iCloud
키체인」). ThisDeviceOnly 가 붙은 설정값으로 넣은 항목은 그 동기화에 끼지 않습니다. 백업을
통해 새 기기로 옮겨 가지도 않습니다.
그래서 사용자가 기기를 바꾸면 토큰이 없어집니다. 서버는 그 사용자에게서 다시 로그인 요청을 받게 됩니다.
항목을 꺼낼 수 있는 앱
기본으로는 항목을 넣은 앱만 그 항목을 찾을 수 있습니다. 운영체제는 부른 앱이 누구인지를 앱의 코드 서명으로 가립니다. 코드 서명은 이 앱을 누가 만들었고 그 뒤로 바뀌지 않았음을 보이는 전자 서명입니다.
그래서 다른 개발자의 앱이 같은 서비스와 계정 이름으로 찾아도 그 항목은 안 보입니다. 같은 개발자의 앱끼리 항목을 나눠 쓰는 방법은 있습니다. 그 방법은 아래 「맞물림」 절에서 봅니다.
맥에서는 다른 앱이 항목을 찾으면 「이 앱에 허용할지」를 사용자에게 묻는 창이 뜰 수 있습니다.
앱을 지워도 남는 항목
iOS 에서 앱을 지워도 그 앱이 넣은 키체인 항목은 지워지지 않고 남을 수 있습니다. 항목이 앱 폴더가 아니라 앱 밖의 저장소에 있기 때문입니다. 그래서 앱을 다시 설치하면 지난번 토큰을 찾아내 로그인한 채로 시작하는 일이 생깁니다.
흔히 쓰는 대처는 이렇습니다. 앱 폴더의 설정 파일에 「한 번 실행했다」는 표시를 남깁니다. 설정 파일은 앱과 함께 지워지므로, 표시가 없으면 새로 설치한 것으로 보고 남은 키체인 항목부터 지웁니다.
키체인이 맡지 않는 일
키체인은 한 기기 안의 보관함입니다. 서버가 쓰는 데이터베이스 비밀번호나 API 키는 키체인이 아니라 서버 쪽의 시크릿 관리 도구가 맡습니다.
키체인은 작은 값을 위한 곳이기도 합니다. 큰 파일을 지켜야 하면 파일은 암호화해 앱 폴더에 두고, 암호화에 쓴 키만 키체인에 넣습니다.
안드로이드에서 비슷한 일을 하는 것은 안드로이드 키스토어입니다. 키스토어는 값보다 암호 키를 지키는 데 무게를 둡니다. 앱은 거기 든 키로 토큰을 암호화해 앱 폴더에 둡니다. 그래서 같은 앱을 두 플랫폼에 낼 때 토큰을 보관하는 코드가 서로 다르게 생깁니다.
맞물림
키체인은 혼자보다 애플의 다른 기능과 붙어서 쓰일 때가 많습니다. 이 절은 그중 넷을 봅니다. 얼굴이나 지문으로 하는 확인, 칩 안의 보안 영역, 코드 서명, 기기 사이의 동기화입니다. 리프레시 토큰 예를 이어서 씁니다.
생체 인증
항목을 넣을 때 앱은 「꺼낼 때마다 사용자를 확인하라」는 조건을 함께 붙일 수 있습니다. 이
조건은 SecAccessControlCreateWithFlags 로 만듭니다. 나중에 앱이 그 항목을 찾으면 운영체제가
얼굴이나 지문을 확인하는 창을 띄웁니다. 이렇게 몸의 특징으로 사람을 확인하는 것을
생체 인증이라고 부릅니다.
sequenceDiagram
participant 앱
participant 운영체제
participant 사용자
앱->>운영체제: 토큰 항목을 찾아 달라
운영체제->>사용자: 얼굴이나 지문을 확인한다
사용자-->>운영체제: 확인을 통과한다
운영체제-->>앱: 토큰
앱 쪽으로는 확인을 통과한 뒤의 토큰만 건너옵니다. 얼굴이나 지문 데이터는 앱에 오지 않습니다. 확인이 실패하면 앱은 토큰 대신 실패 코드를 받습니다.
조건을 「지금 등록된 생체 정보」로 묶을 수도 있습니다. 그러면 사용자가 지문을 새로 등록하는 순간 그 항목은 꺼낼 수 없게 됩니다. 남이 자기 지문을 몰래 더해 꺼내 가는 길은 막히지만, 앱은 사용자를 다시 로그인시켜야 합니다.
Secure Enclave
Secure Enclave는 애플 기기의 칩 안에 따로 떨어져 있는 보안 전용 영역입니다. 앱이 키체인에 키를 만들어 달라고 할 때 이 영역에서 만들라고 지정할 수 있습니다. 그렇게 만든 개인 키는 이 영역 밖으로 한 번도 나오지 않습니다.
앱은 키를 받는 대신 「이 데이터에 서명해 달라」고 키체인에 부탁합니다. 서명은 Secure Enclave 안에서 이뤄집니다. 앱은 결과인 서명만 받습니다. 서버는 기기가 처음 등록할 때 보낸 공개 키로 이 서명을 검증해, 요청이 그 기기에서 왔는지 확인할 수 있습니다.
이 키는 기기 밖으로 옮길 수 없어서 백업에도 안 실리고 새 기기로도 못 갑니다. 사용자가 기기를 바꾸면 새 키를 만들어 서버에 다시 등록해야 합니다.
코드 서명과 접근 그룹
같은 개발자가 낸 앱 둘이 로그인 토큰 하나를 나눠 쓰고 싶을 때가 있습니다. 이때 두 앱이 같은 접근 그룹(access group) 이름을 씁니다. 항목을 넣을 때 그룹 이름을 속성으로 붙이면, 그 그룹에 든 앱은 어느 것이든 그 항목을 찾습니다.
그룹에 들었는지는 앱이 스스로 주장하는 것이 아닙니다. 앱을 빌드할 때 「이 앱은 이 그룹들을 쓴다」는 권한 목록이 코드 서명 안에 함께 서명됩니다. 이 목록을 엔타이틀먼트(entitlement)라고 부릅니다. 운영체제는 부른 앱의 서명에서 이 목록을 읽어 보여 줄 항목을 고릅니다.
그룹 이름 앞에는 개발자 계정마다 붙는 팀 식별자가 들어갑니다. 그래서 다른 개발자 계정의 앱과는 항목을 나눌 수 없습니다. 앱을 다른 계정으로 넘기면 식별자가 바뀌어, 예전에 넣은 항목을 못 찾게 될 수 있습니다.
iCloud 키체인
항목을 넣을 때 kSecAttrSynchronizable 속성을 켜면, 그 항목은 같은 애플 계정으로 로그인한
사용자의 다른 기기로 퍼집니다. 나르는 일은 iCloud 키체인이 맡습니다. 기기 사이를 오가는
동안에도 값은 암호화된 채라서 중간의 애플 서버는 값을 읽지 못합니다.
아이폰에서 로그인한 앱을 아이패드에서 처음 열었는데 이미 로그인돼 있다면 이 동기화 덕분입니다. 사파리에 저장한 비밀번호와 패스키도 이 길로 다른 기기에 퍼집니다.
지우는 것도 함께 퍼집니다. 한 기기에서 로그아웃하며 항목을 지우면 다른 기기에서도 사라집니다.
그리고 이름 끝에 ThisDeviceOnly 가 붙은 설정값으로 넣은 항목은 이 속성을 켤 수 없습니다.
관련 항목
키체인이 보관하는 비밀
비밀번호 · 리프레시 토큰 · 액세스 토큰 · API 키 · 인증서 · 개인 키 · 암호 키 · 패스키 · 자격증명
키체인이 기대는 애플 플랫폼 기능
iOS · macOS · 샌드박스 · 코드 서명 · 엔타이틀먼트 · iCloud 키체인 · 생체 인증 · 데이터 보호 · 푸시 알림
키를 하드웨어 안에 가두는 장치
Secure Enclave · TPM · 하드웨어 보안 모듈 · 신뢰 실행 환경
다른 운영체제에서 같은 역할을 하는 보관함
안드로이드 키스토어 · Windows 자격 증명 관리자 · GNOME 키링 · 비밀번호 관리자
키체인 대신 비밀을 두면 새는 저장소
UserDefaults · 로컬 스토리지 · 쿠키 · 평문 저장
키체인에 넣은 토큰을 쓰는 인증 방식
OAuth 2.0 · JWT · 세션 · 토큰 · 기기 바인딩
서버 쪽에서 비밀을 지키는 수단
시크릿 관리 · 키 관리 · HashiCorp Vault · 암호화 · 공개 키 암호 · 전자 서명
키체인 설계에 깔린 보안 원칙과 위협
다른 이름: Keychain · keychain · 키 체인 · Keychain Services · 키체인 서비스