애플사이다의 iOS 개발 일지

[Keychain] 공식문서 읽는 방법 - 공식문서만으로 키체인을 이해해보자 (긴글 주의) 본문

iOS/영문 공식문서 뜯어보기-iOS

[Keychain] 공식문서 읽는 방법 - 공식문서만으로 키체인을 이해해보자 (긴글 주의)

Applecider 2022. 8. 26. 23:53

공식문서를 읽다 보면 링크를 타고 다른 문서를 읽게 되고,
또 그 안에 링크를 타고 다른 글을 읽어야 하고... 가 반복되기 때문에 지칠 때가 있다.

 

이번 포스트에서는 공식문서들을 효율적으로 떠돌아다니는 방법을 소개하고자 한다.

 

다른 키워드에 비해 Keychain에 대한 공식문서 양이 (그나마) 방대하지 않아서 (← 수박 조언 감사해요)

오직 공식문서만으로 Keychain을 이해해보는 시도를 했다.

아래의 떠돌이 과정 (빨간색 글씨 참고)을 통해 공식문서의 중요도를 판단하는 데 도움이 되었으면 한다.


1. 최상단의 문서 - Keychain Services

Keychain에 대한 설명이 시작되는 문서는 Keychain Services이다.

  • 요약 : Keychain service란 사용자 대신 작은 데이터 덩어리(small chunks of data)를 안전하게 저장하기 위한 기능이다.
  • 이게 왜 필요할까? 사용자가 비밀번호 등의 데이터를 일일이 기억하는 것이 번거롭기 때문이다.
  • Keychain service API는 앱에게 작은 데이터를 Keychain에 저장할 수 있는 메커니즘을 제공한다.
  • Keychain (열쇠고리)은 작은 데이터를 저장하는 “암호화된 데이터베이스” (encrypted database)이다.
  • 작은 데이터란 비밀번호, 신용카드 정보, 짧은 메모 등의 기밀 정보, 그리고 사용자가 인지하지 못하는 암호키 (cryptographic keys), 인증서 등을 말한다.

*다음은 문서 하단 Topics의 API Components의 3개 링크를 따라가 읽으면 된다.

2. API Components

2-1) Keychain Item

  • 요약 : Keychain에 Data (기밀 정보)를 저장할 때, Data를 포장해서 Item 형태로 저장한다.
  • DataAttribute를 함께 포장해서 1개의 Item으로 만든다. Attribute를 통해 Data를 검색/접근한다.
  • Keychain service는 무슨 일을 할까? Data를 암호화하고, Keychain에 Item을 저장하는 것을 관리한다. (Keychain은 디스크에 저장된 “암호화된 데이터베이스”이다.)
  • 나중에 “승인된 프로세스” (authorized processes)를 통해 Keychain service를 사용하여 Item을 찾고 Data의 암호를 해제한다.

*문서 하단 Topics에 First Steps 항목이 있다면 반드시 읽어야 한다. 기초 개념을 다루기 때문이다.
*아래의 Adding Keychain Items, Keychain Item Search, Keychain Item Modification 항목도 중요해보인다.
(Legacy 어쩌구 주제들은 '과거에 이 방식을 사용했구나' 하고 넘어가면 된다.)
*일단 1번 문서와 연결된 나머지 링크들을 먼저 읽자.

2-2) Keychains

  • 요약 : macOS의 전체 Keychain을 생성하고 관리한다.
  • iOS Keychain과 macOS Keychain의 특성이 다르다.
  • iOS
    • 여러 앱들은 single Keychain에 접근할 수 있다.
      *single Keychain이란?
      앱마다 개별적인 Keychain을 가지는 게 아니라, 여러 앱들이 동일한 열쇠고리를 공유한다는 의미이다.
    • 아이폰의 잠금/잠금 해제 상태에 따라 Keychain도 자동으로 아이폰과 동일한 잠금/잠금 해제 상태가 된다.
    • 여러 앱들이 동일한 Keychain을 공유하고 있지만, 1개 앱이 Keychain에 들어있는 모든 Item들에 접근할 수 있는 것이 아니다. 특정 앱은 자신이 가진 Keychain Item에만 접근 가능하다.
      (앱이 소속된 group이 있는 경우, 해당 group의 Item에는 접근 가능하다.)
  • macOS
    • 여러 개의 Keychain을 가질 수 있다.
    • 맥북의 Keychain Access 앱을 통해 사용자가 직접 Keychain을 관리할 수 있고, iOS와 마찬가지로 default Keychain으로 암시적으로 작업할 수 있다. 
    • 그럼에도 Keychain service API는 개발자가 직접 Keychain을 조작 (생성/수정)할 수 있도록 함수를 제공한다.
      ex. 특정 앱 전용의 Keychain을 생성/관리할 수 있다.
    • ”강력한 접근 제어 메커니즘”은 일반적으로 “키 체인 접근 utility”를 복제하려는 앱 이외의 다른 앱에 대해 이것 (Keychain 조작)을 불필요하게 만든다. 

*요약 부분을 때 'macOS에 대한 내용이니까 당장 앱 개발할 때는 필요 없겠구나'를 알 수 있다.
*문서 하단 Topics를 훑어보며 'Creation and Deletion, Search 등이 있구나'를 체크하고 넘어가면 된다.

2-3) Access Control Lists (AC List)

  • 요약 : macOS에서 Keychain Item에 접근 가능한 앱을 제어한다.
  • macOS에서 1개 Keychain ItemAccess 인스턴스를 가진다.
    (Access 인스턴스에는 “AC List”가 들어있다. AC List는 여러 개의 “Entry(항목)”로 구성된다.)
  • Entry에는 Operations 배열 및 (해당 item을 사용하여 operations를 수행할 수 있음을 신뢰할 수 있는) Trusted app 배열이 들어있다.
  • AC List는 해당 Keychain Item에 대한 접근 가능성 (Accessibility)을 제어한다.
  • 이러한 AC List는 어떻게 사용될까?
    특정 앱이 문서에 서명 (sign) 하기 위해 Keychain Item에 접근하려 한다면, 시스템은 AC List를 뒤져서 원하는 operation을 갖고 있는 Entry를 찾아낸다.
    • 만약 해당 Entry가 없으면 시스템은 접근을 거부한다. (거부 당한 앱은 다른 작업을 시도하거나, 사용자에게 알림을 줄 수 있다.)
    • 해당 Entry가 있으면 시스템은 Entry의 Trusted app 배열에 그 앱이 있는지 확인한다.
      - 만약 앱이 있으면 시스템은 접근권한을 부여 (grants access)한다.
      - 앱이 없으면 시스템은 사용자에게 확인 메시지를 표시한다. 사용자는 Deny, Allow, Always Allow 등을 선택할 수 있다. 사용자가 접근을 허용하면, 시스템은 그 앱을 Entry의 Trusted app에 추가한다.
  • Important: AC List는 iOS, 그리고 iCloud Keychain을 사용하는 macOS 앱에는 사용 불가하다.
    이러한 환경에서 Keychain Item을 공유하려면, Access Group을 사용해야 한다.
    (Sharing Access to Keychain Items Among a Collection of Apps 링크를 훑어보면, “하나의 개발 팀”에서 개발한 family apps가 있고, 그 앱들이 동일한 사용자 정보를 활용하는 상황이라면, Access Group을 지정해서 사용자 정보 (Keychain Items)를 공유할 수 있다는 내용이 나온다.)

*문서 하단 Topics에 Access Creation, Access Query 등이 있음을 훑고 넘어간다.
*여기까지 1번 문서 Topics의 API Components를 모두 읽었다. 이제 다시 2-1번 문서에서 봤던 First Steps 링크를 보러간다.

3. Article - Using the Keychain to Manage User Secrets

  • 요약 : 사용자의 작은 기밀정보를 Keychain에 보관하여 사용자가 일일이 기억할 필요가 없게 하자.
  • Keychain service를 통해 사용자는 암호화된 저장소에 쉽게 접근할 수 있다.
    ex. 인터넷 비밀번호를 저장하기 위해 Keychain Item을 사용한 프로세스 예시
    • 아래는 사용자의 인터넷 비밀번호가 인증되면 (End 단계), 네트워크 접근을 할 수 있음을 나타내는 그림이다.
    • 비밀번호가 들어있는 Keychain Item을 Add 또는 Modify하거나 비밀번호를 인증하는 과정을 설명한다.
      • SecItemCopyMatching을 사용하여 Item을 검색한다. Item이 없으면 SecItemAdd를 통해 Item을 Add한다.
      • Item이 있으면 Authenticate(인증)을 진행한다. 인증에 실패하면 SecItemUpdate 을 통해 Item을 Modify한다.

  • Involve the User When Needed (필요시에만 사용자를 통해 입력값을 받음)
    • 앱이 최초로 Credentials (자격 증명)을 필요로 할 때는 Keychain에 저장된 비밀번호가 없다.
    • 이 경우 앱은 사용자에게 메시지를 표시하여 Credentials를 입력받는다. (위 그림의 오른쪽 Flow 참고)
    • 앱은 SecItemAdd 함수를 통해 이 사용자 입력값을 저장한다. 이제 앱은 네트워크 접근을 이어갈 수 있다.
      (본문 링크는 중요하므로 꼭 확인해야 한다.)
      • 참고 - 오른쪽 Flow에서 사용자 입력값에 대한 Authenticate (인증)이 필요한 이유는?
        - 이 상황에서 '인증'이란 사용자의 id/pw가 일치하는지 서버에게 물어보는 것이다. 
        - mac에서 사용하던 웹사이트를, 처음으로 앱에서 로그인할 때, id/pw는 있고, 키체인은 없을 때 오른쪽 Flow를 탄다.
        - 제대로 된 사용자 입력값을 받을 때까지 Item Add를 하지 않는다.
    • 나중에 서버가 재인증 (reauthentication)을 요구할 때, 앱은 사용자를 거치지 않고 Keychain에 저장한 Credentials를 꺼내올 수 있다.
    • Keychain에 Item을 추가하는 방법은 Adding a Password to the Keychain 문서를 확인하면 된다.
      (마찬가지로 본문 링크이므로 꼭 확인해야 한다.)
  • Avoid Bothering the User in the Common Case
    • 사용자로부터 비밀번호를 입력받거나, 새 비밀번호를 설정하는 등의 interaction을 최소화하는 것이 좋다.
      따라서 일반적으로 가운데 Flow를 따른다.
    • 사용자가 앱을 오랜만에 사용했을 때 등의 상황에서 Secure network resource는 주기적으로 reauthentication을 요구한다.
    • 이에 대응하기 위해 앱은 SecItemCopyMatching 함수를 통해 Keychain에서 해당 비밀번호를 찾는다.
      이 비밀번호로 인증이 성공하면 사용자 interaction이 필요 없어서 좋다.
  • Handle Changes Gracefully
    • 가끔 사용자가 앱 영역 밖에서 비밀번호를 수정할 수 있다. (ex. 웹사이트에서 사용자가 비밀번호를 변경하는 경우)
    • 이때 앱의 subsequent keychain item을 찾아서 얻은 비밀번호는 수정 이전의 비밀번호이므로 인증에 실패하게 된다.
    • 이 경우 사용자를 통해 새로운 비밀번호를 받고, SecItemUpdate 함수를 통해 기존 저장값을 수정한다.
      (위 그림의 왼쪽 Flow 참고)
    • SecItemDelete 함수를 통해 Keychain에서 비밀번호를 완전히 삭제할 수 있다.

*문서 하단 Topics의 Item Creation and Modification의 3개 링크가 중요해 보인다. (2-1번 문서의 상위 Topic에도 있었던 링크)
*See Also의 First Steps 링크를 먼저 보는 것이 좋다. 더 기초적인 개념일 테니..
  (위 링크를 따라가도 좋지만, 어차피 본문에 First Steps 링크가 들어있다.)

4. Class - SecKeychainItem

  • 요약 : Keychain을 나타내는 opaque 타입이다.
    *opaque 타입이란?
    반환타입이 Opaque 타입인 함수는 반환 정보를 외부에 숨길 수 있다.
    예를 들어 함수의 반환타입이 Opaque 타입이라면, "A 프로토콜을 채택한 타입"이면 모두 가능하도록 설정할 수 있다.
  • Keychain에 저장된 certificate (인증서)에 대한 SecKeyChainItem 객체는 SecCertificate로 안전하게 캐스팅될 수 있다.
    (본문 링크인 SecCertificate을 훑어보면, 특정 인증서를 나타내는 CoreFoundation 추상 타입의 객체임을 알 수 있다.)
    *인증서 관련 개념은 나중에 찾아보기로 하고 일단 패스...
    (Apple은 네이밍을 신경 쓴다. 클래스 이름을 먼저 확인하자. 클래스 요약을 보면 이 내용은 OS, 네트워크 프로토콜 관련 내용이므로 일단 패스해도 된다고 유추할 수 있다.)
     

*문서 하단 Topics에 또 First Steps가 있다. 젠장...
그래도 하나는 이미 읽은 링크이고, 하나는 2번 문서의 First Steps 마지막 링크와 일치하니까 다행이다..

이제 이 마지막 링크를 보자.

5. Function - SecKeychainItemGetTypeID()

  • 요약 : Keychain Item 객체가 속한 Opaque 타입의 identifier를 반환한다.
  • 이 함수의 반환값을 CFTypeID identifier와 비교할 수 있다. CFTypeID identifier는 CFGetTypeID(_:) 함수의 반환값이다.
    (본문 링크를 훑어보면 CFTypeID는 UInt의 typealias이고, Core Foundation에서 타입 identifier를 정의하며, 타입 identifier란 CoreFoundation 객체가 "속해 있는" Opaque 타입을 식별하는 UInt 값임을 알 수 있다. 대충 넘어가자..)
  • 이 함수의 반환값은 release 마다, platform 마다 변경될 수 있다. 

*문서 하단 See Also의 First Steps는 이미 읽은 링크들이다.
이제 3번 문서의 Item Creation and Modification의 링크를 따라간다. 이게 본론인 듯. 드디어 코드가 나온다..

6. Article - Adding a Password to the Keychain

  • 요약 : 사용자를 대신해서 Keychain에 네트워크 Credentials (자격 증명)을 추가하는 방법을 설명한다.
  • Keychain service를 사용하면 사용자 비밀번호를 간단히 저장할 수 있다.

(1) Get Set Up (설정하기)

  • 앱에서 사용할 Credentials 정보를 저장할 구조체를 정의해라.
  • 그다음, Keychain 접근 결과를 나타내기 위한 Error 열거형을 정의해라.
  • 그리고 앱의 서버를 입력해라.
struct Credentials {
    var username: String
    var password: String
}
enum KeychainError: Error {
    case noPassword
    case unexpectedPasswordData
    case unhandledError(status: OSStatus)
}
static let server = "www.example.com"

(2) Create an Add Query

  • 위에서 정의한 Credentials 구조체의 인스턴스, server 상수를 사용하여 Add query를 생성해라.
let credentials = Credentials(username: "Applecider", password: "1234")
let account = credentials.username
let password = credentials.password.data(using: String.Encoding.utf8)!  // 비밀번호는 Data 타입으로 인코딩함
var query: [String: Any] = [kSecClass as String: kSecClassInternetPassword,
                            kSecAttrAccount as String: account,
                            kSecAttrServer as String: server,
                            kSecValueData as String: password]
  • 위 코드의 query 딕셔너리를 자세히 보자.
  • 첫 번째 key-value : kSecClass가 있는 key-value는 “Item이 어떤 종류의 정보인지” 나타낸다.
    예시의 value인 kSecClassInternetPassword"Item이 Internet password (인터넷 비밀번호)임"을 나타낸다.
    → ❗Keychain service는 이것을 보고, Data가 기밀 정보이고 암호화를 필요로 한다고 추론한다. 또한 Item이 Attribute를 가지는 것을 보장한다. 여기서 Attribute는 다른 인터넷 비밀번호와 이 Item을 구별하는 역할을 한다.
    (다른 인터넷 비밀번호의 Attribute와 이 Item의 Attribute가 구별된다는 뜻인 듯)
    This also ensures that the item has attributes that distinguish it from other Internet passwords, such as the server and account to which it applies.
  • 두세 번째 key-value : 위에서 말한 Attribute 정보를 제공하고 있다.
    (상수 account를 통해 사용자의 이름을, 상수 server를 통해 도메인 이름을 첨부한다.)
  • 가장 중요한 비밀번호는 Data 인스턴스로 인코딩하여 query에 넣는다.
  • Note: Keychain service는 관련된 kSecClassGenericPassword 를 제공한다. Generic password (일반적인 비밀번호, 제네릭 타입 아님)는 Internet password와 비슷하지만, remote access와 관련된 Attribute가 없다. kSecAttrServer 등의 Attribate가 필요 없을 때 사용하면 된다.
    • (본문 링크를 타고 kSecClassGenericPassword 를 훑어보면, 일반적인 비밀번호 Item을 나타내는 “값”이며, 전역 변수임을 알 수 있다. 아래의 Attribute들은 이 클래스의 Item에 적용된다. 그리고 이 전역변수의 타입은 CFString 클래스인데, Core Foundation에 속한다. iOS 개발에 대부분 사용되는 Foundation 프레임워크의 Definition을 뜯어보면 이 Core Foundation을 import하고 있다.)
    • 참고 - 접두어 k- 는 왜 붙일까? (참고: What does the 'k' prefix indicate in Apple's APIs?)
      • The k means “constant” in hungarian notation. (Hungarian noun “konstans” means “constant”.)
      • macintosh 프로그래밍 초기부터 사용한 네이밍 컨벤션이다.

(3) Add the Item

let status = SecItemAdd(query as CFDictionary, nil)
guard status == errSecSuccess else { throw KeychainError.unhandledError(status: status) }
  • query가 완성되면 SecItemAdd 함수에 전달하면 된다.
    *본문 링크는 중요하니까 SecItemAdd 함수에 대한 본문 링크를 먼저 확인해보자. (7번 문서 요약 참고)
  • 일반적으로 Add operation의 두 번째 argument에서 참조 형태로 제공되는 return data를 무시한다.
    하지만 함수의 return status를 확인하여 operation이 성공했는지 항상 확인하는 것이 좋다.
    ex. 주어진 Attribute를 가진 Item이 이미 존재하는 경우 Add operation이 실패할 수 있다.

(4) Ensure Searchability

  • 나중에 Item을 검색하려면, Attribute를 활용해야 한다.
  • 위 예시에서 server 및 account는 Item을 다른 Item과 구별하는 특징이다.
  • server와 같은 constant attribute는 검색하는 동안 동일한 값을 사용하지만, account는 런타임 때 사용자가 제공한 값을 가지고 있으므로 dynamic attribute이다.
    앱이 다양한 Attribute를 가지는 유사한 Item을 추가할 일이 없다면, dynamic attribute를 search 매개변수에서 생략할 수 있다.
    *뒷부분은 나중에 보자...

*(본문 링크 SecItemAdd 함수를 봤다면) Add Item을 다 봤으니 Search Item 부분을 읽어보자. (9번 문서 요약 참고)

7. Function - SecItemAdd(_:_:)

func SecItemAdd(_ attributes: CFDictionary, 
              _ result: UnsafeMutablePointer<CFTypeRef?>?) -> OSStatus
  • 요약 : 1개 이상의 Item을 Keychain에 추가한다.
  • 문서가 길지만.. 침착히 매개변수 attributes, result 그리고 반환타입을 알아보자.
  • attributes 매개변수
    • 타입은 딕셔너리 (CFDictionary)이다. (CFDictionary 클래스는 immutable 딕셔너리 객체에 대한 참조이다.)
    • 딕셔너리의 역할은 Keychain에 추가할 Item을 설명하는 것이다. query가 이 매개변수에 전달된다.
    • ✅ query에는 Item의 class, data, atrribute 정보가 담겨있다.
      1. Item의 Class
        • Items의 Classes에 따라 다른 Attribute와 behavior이 활용된다. kSecClass라는 key에 대한 value는 무엇을 의미할까? ✅ value는 Keychain service에게 “저장할 Data가 어떤 종류인지” 알려주는 역할을 한다. 저장할 Data가 비밀번호인지, certificate인지, cryptographic key인지 나타낼 수 있다. (ex. kSecClassGenericPassword는 저장할 데이터가 일반적인 비밀번호임을 알려준다.)
        • 본문 링크의 kSecClass를 보면, 딕셔너리의 key이고 value는 Item의 Class를 나타내는 전역 변수임을 알 수 있다.
        • Item Class Keys and Values를 보면, Keychain Item의 종류 (비밀번호, certificate 등)에 따라 Class를 지정해야 함을 알 수 있다.
          *문서 하단 Topics의 Item Class의 Key와 Value로 들어갈 수 있는 k- 상수들을 확인할 수 있다!
          훑어보니 중요한 문서인 듯 (8번 문서 요약 참고)
      2. Data
        • ✅ 암호화시켜서 저장하게 될 데이터를 의미한다. kSecValueData라는 key를 사용하고, key의 value에 저장할 Data (ex. 비밀번호)를 넣는다. Keychain service는 Item의 종류에 따라 이 Data를 암호화할지 말지를 결정한다. Item이 기밀정보라면 (즉, 비밀번호 타입이거나 private key를 포함하고 있는 경우) Data를 암호화한다.
        • 본문 링크의 kSecValueData를 보면, 이 딕셔너리 key의 value는 CFData 타입이다.
      3. Attribute (optional)
        • Attribute key 정보를 말한다. ✅ 이 key는 나중에 Item을 검색할 수 있게 해주고, Data가 사용되거나 공유되는 방법을 나타낸다. 원하는 만큼 Attribute를 추가할 수 있지만, Item Class에 따라 추가할 수 있는 Attribute의 종류가 정해져 있다.
        • 본문 링크의 Item Attribute Keys and Values를 보면, Attribute 정보를 나타내기 위한 key/value를 확인할 수 있다. (ex. 아래의 Topics의 Password Attribute Keys를 보면 저장할 Data가 비밀번호인 경우에 사용 가능한 Attribute key들이 나와있다.)
      4. 반환 타입 (optional)
        • 1개 이상의 반환 타입 key를 말한다. 이 key가 의미하는 것은 ✅ 성공적으로 완료되면 (SecItemAdd 메서드를 통한 Add Operation이 완료되면) 반환되도록 할 Data가 무엇인지이다.
          → 이걸 언제 사용할까?
          “Keychain”에 Item을 추가하는 것 (Add Operation)과 별개로 “서버”에도 정보를 보내서 저장하게 할 수도 있다. 이때 서버에 보내는 작업을 할 때 정보를 result 매개변수에 담아서 보내면 된다. (Keychain을 통해 암호화한 비밀번호를 result에 담아서 서버에게 보낸다는 뜻이다.) 그리고 서버에 정보를 보낼 때 Item에 담긴 기밀정보를 모두 보내도 되지만, 기밀정보가 많다면 원하는 정보만 필터링해서 보낼 수도 있다. 보통 SecItemAdd 함수의 반환값을 무시하는데, 이때는 result 매개변수에 반환값 key를 넣을 필요가 없다.
        • 본문 링크의 Item Return Result Keys를 보면, SecItemAdd 함수는 result 매개변수를 통해 Item의 Data 및 Attribute를 반환하고, 이 result 매개변수에게 pointer를 제공한다. (???) query 딕셔너리에 Item result key를 사용하여 result를 어떤 형태 (format)로 나타낼지 지정한다. (???)
          *이 부분을 이해하기 힘들었다. 이 함수의 result 매개변수를 먼저 보는 것이 좋겠다.
  • result 매개변수
    • 타입은 UnsafeMutablePointer<CFTypeRef?>? 이다.
    • CFTypeRef는 AnyObject의 typealias이다.
      (Core Foundation에서 정의된 base 타입이다. polymorphic 함수 (???)의 반환(타입)으로 사용된다.)
    • UnsafeMutablePointer는 Generic Structure @frozen struct UnsafeMutablePointer<Pointee> 이다.
      이 인스턴스를 사용하여 메모리에 있는 특정 타입 (Pointee 타입)의 데이터에 접근할 수 있다.
    • 반환 시 (SecItemAdd 메서드의 반환) 새롭게 추가된 Item에 대한 참조이다. 이 매개변수의 정확한 타입은 attribute 매개변수의 특정 value에 따라 결정된다. (attribute 매개변수의 4. 반환 타입 key의 value를 말하는 듯)
    • result가 필요 없으면 nil을 할당하고, 만약 필요하면 앱 내부에서 참조한 객체를 메모리에서 해제해줘야 한다. (???)
    • 참고 - result는 왜 포인터/참조 사용할까?
      사실 정답이 없는 문제이다. 값을 전달해도 되지만, 여기서는 참조를 넘겨주도록 구현했을 뿐이다.
  •  반환값
    • “result code”라고만 설명되어 있다.
    • 타입은 OSStatus 이다. (근데 이상하게도 OSStatus 타입은 링크가 없다..)
    • 본문 링크의 Security Framework Result Codes를 보면, Evaluate result codes common to many Security framework functions... 보안 관련 기능인가..?
    • OSStatus을 구글링하면, 특정 함수의 “result code”라고 나온다. Keychain 관련 오류라고 봐도 될 듯?
    • OSStatus 의미를 찾아주는 사이트: https://www.osstatus.com/
  • Discussion
    • Keychain에 여러 Item을 한꺼번에 추가하려면, kSecUseItemList key를 사용하고, value에 딕셔너리들의 배열을 넣으면 된다. 단, 비밀번호가 아닌 Item만 가능하다.
    • Xcode는 application-identifier entitlement (자격)을 앱 bundle에 추가한다. Keychain service는 이 entitlement를 사용하여 앱의 Keychain Item에 대한 접근권한을 부여한다.

*이제 attributes 매개변수 부분의 본문 링크인 Item Class Keys and Values를 살펴보자.

8. Item Class Keys and Values

  • 요약 : Keychain Item의 Class를 지정해라.
  • Keychain Item의 종류 (ex. 비밀번호, certificate 등)에 따라 Class를 지정해야 한다.
  • Item의 Class는 무엇을 의미할까? 적용할 Attribute를 정하고, 시스템에게 저장할 Data를 암호화할지 말지를 결정하게 한다.
    (ex. 비밀번호는 암호화하고, certificate은 암호화하지 않는다.)
  • key와 아래 목록의 value를 사용하여 새로 추가할 Item의 Class를 지정한다. (Item을 추가하려면 SecItemAdd 함수를 호출하면서 attributes 매개변수에 key/value 딕셔너리를 전달하면 된다.) ← 이렇게 보니 공식문서가 친절하긴 하다...
  • 나중에 Item을 검색 (search)할 때는 query 딕셔너리의 동일한 key/value를 사용하면 된다. 이때 SecItemCopyMatching(_:_:)SecItemUpdate(_:_:)SecItemDelete(_:) 함수 중 하나를 호출한다.

*아래 Topics의 Item Class의 Key와 Value로 들어갈 수 있는 k- 들을 확인할 수 있다!

9. Article - Searching for Keychain Items

  • 요약 : 개발자가 지정한 검색 criteria (기준)에 따라 Keychain Item을 검색한다.
  • Keychain Item을 검색하려면 query 딕셔너리를 사용하면 된다. ❗query 딕셔너리는 Keychain service API에게 어떤 Item Attribute를 검색할지, match (일치하는 항목)을 찾으면 무엇을 반환할지 알려준다.
    • Attribute가 있어야 data를 찾을 수 있다!
      정확히는 Keychain Item이 아니라 ”search query에 Attribute가 있어야” 제대로 반환된다.
  • 또한 검색을 구체화하기 위해 개발자는 query 딕셔너리를 통해 추가적인 매개변수를 지정할 수 있다.
    ex. 문자열 Attribute를 match하거나, 일치 항목의 개수를 제한하고 싶은 경우, case sensitivity (대소문자 구분 여부) 등을 제어하면 된다.
  • Keychain에 비밀번호를 추가하는 저번 예시를 생각해보자. 사용자는 네트워크 서비스에 대한 Credentials (앱에 저장됨)를 제공하고, 앱을 사용하다가 다른 작업으로 넘어간다. 사용자가 다시 앱을 사용할 때, 앱이 계속 작동하려면 서버에서 reauthenticate이 필요할 수 있다. 이때 앱은 Keychain을 통해 비밀번호를 load한다.

 

(1) Create a Search Query

let query: [String: Any] = [kSecClass as String: kSecClassInternetPassword,
                            kSecAttrServer as String: server,  // 이 Attribute를 가지는 Item을 검색하겠음
                            kSecMatchLimit as String: kSecMatchLimitOne,  // 검색 결과를 limit함
                            kSecReturnAttributes as String: true,  // Item을 찾으면 Attribute와 Data를 꺼내겠음
                            kSecReturnData as String: true]
  • query 딕셔너리를 구성하여 검색을 시작한다.
  • 이 query는 server attribute가 일치하는 인터넷 비밀번호 Item을 검색한다. (10번 문서 요약 참고)
  • kSecMatchLimit라는 search 매개변수를 통해 result (검색 결과로 나온 Item)를 single value로 제한한다.
    이 경우 keychain에서 찾은 첫 번째 항목만 얻게 된다.
    *SecItemCopyMatching 메서드 참고 - query 매개변수의 구성요소 중 search 매개변수가 있다.
  • 이 query는 Item의 Attribute 및 Data를 모두 요구하고 있다.
    둘 다 필요한 이유는 kSecAttrAccount attribute가 사용자 name을 포함하고 있고, Item의 Data가 비밀번호 자체를 가지고 있기 때문이다. (Item Add query에서 사용한 Attribute를 말함)

(2) Initiate the Search

var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &item) // add 함수와 달리 result 매개변수가 nil이 아님
guard status != errSecItemNotFound else { throw KeychainError.noPassword } // 비밀번호를 저장한 적이 없는 경우 발생하는 error
guard status == errSecSuccess else { throw KeychainError.unhandledError(status: status) }
  • SecItemCopyMatching 함수를 호출하여 검색을 시작한다. *본문 링크를 따라 이 함수를 먼저 알아보자. (10번 문서 요약 참고)
  • 먼저 반환된 status 값을 테스트해야 한다. match되는 Item이 없으면 error가 발생할 수 있다.
    ex. 주어진 server에 비밀번호를 이전에 추가하지 않은 경우
  • 이 경우 error가 발생하면 errSecItemNotFound result를 얻게 된다.
    이 error를 통해 앱의 로그인 Flow를 처음 진행하고 있음을 감지할 수 있다.
  • 검색에 성공하면 SecItemCopyMatching 메서드는 item 매개변수를 통해 result (검색 결과)를 제공한다.
    반환된 Item의 타입은 query의 특성에 따라 다르다.

(3) Extract the Result

  • 검색할 때 여러 반환 타입을 요청했고 (Item의 Attribute 및 Data를 요청했음), single result로 제한했으므로 result가 딕셔너리일 것을 예상해야 한다.
  • result를 [String: Any] 타입으로 타입 캐스팅하면, 검색해서 찾은 Item에 접근할 수 있다.
  • kSecAttrAccount key와 관련된 attribute 값으로부터 username을 복구할 수 있다.
  • kSecValueData key를 사용하여 비밀번호 Data를 추출할 수 있다. (추출해서 String 타입으로 인코딩하면 된다.)
guard let existingItem = item as? [String : Any],
    let passwordData = existingItem[kSecValueData as String] as? Data,
    let password = String(data: passwordData, encoding: String.Encoding.utf8),
    let account = existingItem[kSecAttrAccount as String] as? String 
else {
    throw KeychainError.unexpectedPasswordData
}
let credentials = Credentials(username: account, password: password)

*위에서 말했듯이 SecItemCopyMatching 함수를 알아볼 차례이다.

10. Function - SecItemCopyMatching(::)

  • 요약 : search query와 match되는 1개 이상의 Keychain Item을 반환하거나, 특정 Item의 Attribute를 복사한다.
  • query 매개변수
    • SecItemAdd 메서드와 구조가 유사하다. CFDictionary 타입이다.
      1. Item의 Class (SecItemAdd 메서드와 동일하므로 생략)
      2. Attribute
        • 검색할 Item이 가져야 할 Attribute를 지정하여 검색 범위를 좁힌다!
      3. Search 매개변수
        • 다양한 방법으로 검색의 조건을 설정한다. ex. result (검색 결과)를 특정 개수의 Item으로 제한할 수 있다.
        • Search Attribute Keys and Values 문서를 통해 특정 조건을 설정하기 위한 key/value를 확인 가능하다.
      4. 1개 이상의 반환 타입
        • 검색할 대상이 무엇인지 나타낸다. 이때 대상은 the item’s attributes, the item’s data, a reference to the data, a persistent reference to the data, or a combination of these 등으로 지정할 수 있다.
          → 검색할 대상이 항상 Data가 아닐 수 있고, 2번의 검색 범위를 좁히는 Attribute를 활용해서 Item을 찾고, 그 내부의 다른 Attribute를 반환하도록 할 수 있다는 뜻
          → Item에 든 내용이 많을 수도 있는데, 그중에 원하는 값들을 선별해서 받아올 수 있다.
        • Item Return Result Keys 문서를 통해 적절한 key/value를 확인 가능하다.
        • 만약 1개 이상의 반환 타입을 지정하면, 검색 결과로 요청한 타입들을 가지고 있는 딕셔너리를 반환해준다.
        • 만약 여러 개의 result를 검색하도록 허용했다면, Item 배열 형태로 반환된다.
  • result 매개변수
    • UnsafeMutablePointer<CFTypeRef?>? 타입이다.
    • 반환 시 (SecItemCopyMatching 메서드의 반환), 검색하여 찾은 Item에 대한 참조이다.
      (찾은 Item의 주소를 가리키는 포인터 타입)
      • 아래 코드를 보자.
      • 첫째 줄 : 변수 item의 타입은 CFTypeRef? 이다. 참조 (주소)를 저장하겠다는 뜻이다.
      • 둘째 줄 : SecItemCopyMatching 메서드를 통해 검색하여 찾은 Keychain Item의 참조를 변수 item에 할당한다. 근데 왜 item 앞에 &가 붙어있을까? (포인터가 가리키는) 참조를 나타내기 위해서이다.
        → 이때 Data이든 Attribute이든 “복사값” (참조가 아니라)을 반환한다는 게 중요하다. &item의 원본은 키체인에 있다!
        키체인 참조를 직접 주면 함부로 수정할 수 있어서 위험하기 때문이다.
var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &item)

 

  • 반환값
    • OSStatus 타입이다. result code이다.
  • Discussion
    • default로 첫 번째로 match된 Item을 반환한다.
      *나머지 내용은 다음에...

*아까 보기로 했던 링크를 확인해보자.

11. Article - Updating and Deleting Keychain Items

  • 요약 : 사용자 데이터가 변경됐을 때 Keychain Item을 수정한다.
  • Overview
    • 사용자가 앱 외부에서 비밀번호를 변경 (ex. 웹 사이트에서 변경)하면, 앱 Bundle에 저장된 Keychain의 비밀번호와 일치하지 않으므로 로그인에 실패하게 된다. 이 경우 사용자로부터 새로운 Credentials를 입력받아 Keychain에 변경사항을 반영해야 한다.
    • 이때 SecItemAdd 메서드를 사용하면 안 된다. (기존에 Keychain에 등록해둔 Item과 동일한 Attributes를 가지는 Item은 공존할 수 없기 때문이다.) 기존의 Item을 업데이트해야 한다.

(1) Prepare a Search Query and New Attributes

// search query
let query: [String: Any] = [kSecClass as String: kSecClassInternetPassword,  
                            kSecAttrServer as String: server]

// 업데이트할 새로운 Item
let account = credentials.username
let password = credentials.password.data(using: String.Encoding.utf8)! // String->Data로 전환한 비밀번호
let attributes: [String: Any] = [kSecAttrAccount as String: account,   
                                 kSecValueData as String: password]
  • Item을 업데이트하려면 두 가지가 필요하다.
  • 먼저 1) search query를 통해 Item을 찾아야 한다.
    (이건 implicit search이고, SecItemCopyMatching 메서드는 explicit search라고 부름)
    위 예시는 kSecClass, kSecAttrServer 두 가지에 부합하는 Item을 검색하도록 했다.
  • 그다음 2) 업데이트할 새로운 Item 정보가 담긴 딕셔너리를 준비해야 한다.
    이때 사용자의 Credentials 중에서 account 정보는 동일하고, password만 바꾸는 것도 가능하다.

(2) Execute an Update

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) }
  • 이제 SecItemUpdate 메서드를 호출하면, 작성한 Attribute에 해당하는 모든 Item이 업데이트된다. (12번 문서 요약 참고)
  • 마찬가지로 반환된 OSStatus를 확인해야 한다.

(3) Delete Items That You No Longer Need

let status = SecItemDelete(query as CFDictionary)
guard status == errSecSuccess || status == errSecItemNotFound else { throw KeychainError.unhandledError(status: status) }
  • 필요 없는 Item은 Keychain에서 삭제하면 된다. (ex. 사용자가 앱에서 서버 로그아웃할 때)
  • Item 업데이트할 때 사용한 search query를 사용해서 SecItemDelete 메서드를 호출하면 된다. (13번 문서 요약 참고)
  • default로 Keychain service는 search query에 match되는 모든 Item을 삭제한다.
    특정 Item만 삭제하려면 search query에 kSecMatchItemList key를 추가하면 된다.

*마지막으로 update / delete 관련 메서드를 보자.

12. Function - SecItemUpdate(::) 

  • 요약 : search query에 match되는 Item을 수정한다.
  • query 매개변수
    • (Item을 add/search하는 메서드와 마찬가지로) CFDictionary 타입이다.
    • 업데이트하려는 Keychain Item에 대한 search query이다.
  • attributesToUpdate 매개변수
    • CFDictionary 타입이다.
    • 1) 업데이트하려는 값에 대한 Attribute, 2) 새로운 값을 포함하는 딕셔너리이다.
    • meta Attribute (???)는 이 딕셔너리에 넣을 수 없다.
  • 반환값
    • result code이다.

13. Function - SecItemDelete(_:)

  • 요약 : search query에 match되는 Item을 삭제한다.
  • query 매개변수
    • 삭제하려는 Keychain Item에 대한 search query이다.
  • 반환값
    • result code이다.
  • Discussion
    • default로 search query에 해당하는 모든 Item을 삭제한다.
    • key를 추가하면 특정 조건의 Item만 삭제하도록 변경할 수 있다.

 

- Reference

 

🍎 포스트가 도움이 되었다면, 공감🤍 / 구독🍹 / 공유🔗 / 댓글✏️ 로 응원해주세요. 감사합니다.

Comments