Keychain service란 유저를 대신하여 작은 데이터를 보안적으로 안전하게 저장하는 서비스입니다.
Overview
컴퓨터 유저들은 보안적으로 안전하게 저장하고 싶은 데이터가 존재합니다. 예를 들어 수 많은 계정의 아이디와 비밀번호 같은 데이터가 있습니다. 모든 계정의 아이디와 비밀번호는 각각 다를 수 있으며 기억하기 어려워지는데 하지만 종이같은 곳에 적기에는 안전하지 않을 수 있습니다. 유저들은 이런 이유로 계정을 간단하게 설정하는 경우가 있는데 이 또한 보안적으로 안전하지 않습니다.
Keychain service API는 keycahin이라고 불리는 암호화된 데이터베이스에 작은 사용자 데이터를 저장할 수 있는 메커니즘을 제공하여 이 문제를 해결하는 데 도움을 줍니다. 이렇게 keychain를 통해 복잡한 계정을 만들더라도 사용자가 사용할 수 있게 합니다.
keychain 은 비밀번호 뿐만 아니라 유저가 안전하게 저장해야할 다른 데이터 예를들어 신용카드 정보나 토큰들도 저장할 수 있습니다. 심지어 유저가 필요한 데이터이긴 하지만 신경 쓸 필요는 없는 Certificate, Key, and Trust Services로 관리하는 암호화 키와 인증서를 통해 사용자는 통신을 안전하게 할 수 있으며 다른 사용자 및 장치와의 신뢰를 구축할 수 있습니다. 다음 그림과 같은 데이터들을 저장할 수 있습니다.
iOS에서 앱은 단일 키체인(논리적으로 iCloud 키체인을 포함)에 액세스할 수 있습니다. 이 키체인은 사용자가 장치를 잠금 해제하면 자동으로 잠금 해제되고 장치가 잠겨 있을 때 잠깁니다. 앱은 자체 키체인 항목 또는 앱이 속한 그룹과 공유된 항목에만 액세스할 수 있습니다. 키체인 컨테이너 자체를 관리할 수 없습니다.
만일 사용자가 비밀번호나 암호화 키를 저장하고 싶을 경우 저장할 것을 keychain item으로 묶습니다. 데이터 자체뿐만 아니라 item의 접근성을 제어하고 검색 가능하게 만들기 위해 공개적으로 표시되는 속성 세트를 제공합니다. keychain service는 keychain의 데이터를 암호화하고 저장합니다. 이 keychain은 디스크에 저장된 암호화된 데이터베이스입니다. 나중에 권한이 있는 프로세스는 키체인 서비스를 사용하고 항목을 찾고 해당 데이터를 복호화합니다. 다음 그림처럼 Keychain service는 keychain에 대한 데이터 암호화 및 저장을 처리합니다.
유저가 필요할때 끌어들이기(Involve)
앱이 처음으로 자격 증명(비밀번호)이 필요한 경우, 키체인에는 암호가 저장되어 있지 않습니다. 여기서 앱은 오른쪽 그림에 나와 있는 것처럼 사용자에게 프롬프트를 표시합니다. 이후 유저가 성공적으로 인증을 확인하는 자격 증명(비밀번호)을 제공한 후, SecItemAdd(_:_:) 함수를 호출하여 유저가 제공한 자격 증명(비밀번호)을 저장합니다. 이제 앱은 정상적인 네트워크 엑세스를 계속할 수 있게 됩니다. 나중에 서버에서 다시 인증이 필요한 경우, 앱은 사용자에게 번거롭게 하지 않고 대신 키체인에서 자격 증명(비밀번호)을 검색할 수 있습니다.
유저를 비슷한 상황으로 귀찮게 안하기
위에 그림에서 가장 일반적인 많이 사용되는 경로는 사용자 상호 작용이 필요하지 않은 중앙 경로입니다. 안전한 네트워크 자원(예: 토큰)은 주기적으로 다시 인증을 필요로 할 수 있습니다. 예를 들어 사용자가 잠시 동안 앱을 종료한 후 다시 시작하는 경우 입니다.
이에 대응하여 앱은 SecItemCopyMatching(_:_:)함수를 사용하여 키체인에서 암호를 검색합니다. 만약 암호를 찾고 앱이 이를 성공적으로 사용하여 인증하는 경우, 사용자에게 비밀번호를 다시 입력하는 것처럼 귀찮게 하지 않고 진행할 수 있습니다.
우아하게 변경 사항 다루기
가끔 사용자는 앱에서 벗어나 자격 증명(비밀번호)을 변경할 수 있습니다. 예를 들어 사용자가 동일한 서비스에 대한 웹 인터페이스를 제공하고 비밀번호를 변경하거나 재설정할 수 있는 경우입니다. 이런 상황처럼 웹 인터페이스에서 비밀번호를 수정하는 경우 앱의 키체인 항목 검색은 인증에 실패하는 만료된 암호가 됩니다. 그림의 왼쪽 가지가 이러한 시나리오를 처리합니다.
SecItemCopyMatching(_:_:)함수를 통해서 키체인에서 암호를 검색하여 가져온 암호가 Authenticate 순서에서 걸려 앱은 사용자에게 새 자격 증명(비밀번호)을 요청합니다. 다음으로 앱이 유저로부터 웹 인터페이스를 통해 변경된 비밀번호로 인증을 받은 후 변경된 자격 증명(비밀번호)을 유효성 검사한 후 기존 키체인에 저장된 자격 증명(비밀번호)을 수정하기 위해 SecItemUpdate(_:_:) 함수를 호출합니다.
사용자는 자격 증명(비밀번호)를 다시 입력하여 변경된 자격 증명(비밀번호)으로 기존 키체인에 있던 값을 업데이트하는 대신 전체적으로 네트워크 서비스와의 연결을 끊기로 결정할 수도 있습니다. 이에 대응하여 앱은 관련 자격 증명을 잊어버리고 로그아웃에 필요한 기타 작업을 수행해야 합니다. SecItemDelete(_:) 함수를 사용하여 키체인에서 암호를 완전히 제거합니다.
기존 항목을 변경하고 제거하는 데 대한 토론은 "키체인 항목 업데이트 및 삭제"를 참조하십시오.
keychain에 비밀번호 추가하기
사용자를 대신하여 키체인에 네트워크 자격 증명을 추가합니다. keychain service는 사용자의 비밀번호를 간단하고 안전하게 저장하는 것을 가능하게 합니다. 키체인에 유저의 비밀번호를 저장하여 직접 비밀번호 암호화를 구현할 필요가 없습니다.
시작하려면, 메모리에서 앱을 이동할 때 자격 증명을 보관할 구조체를 정의해야합니다.
struct Credentials {
var username: String
var password: String
}
다음으로 error enumeration을 정의하여 keychain으로부터 받은 결과를 확인할 수 있습니다.
enum KeychainError: Error {
case noPassword
case unexpectedPasswordData
case unhandledError(status: OSStatus)
}
그런 다음 앱과 통신을 하는 서버의 URL을 정의합니다
static let server = "www.example.com"
keychain에 저장하기 위한 query 만들기
Credentials 구조체 인스턴스와 server 의 주소가 정의된 상수를 사용하여 add query를 만들 수 있습니다.
let account = credentials.username
let password = credentials.password.data(using: String.Encoding.utf8)!
var query: [String: Any] = [kSecClass as String: kSecClassInternetPassword, // keychain Item Class
kSecAttrAccount as String: account, // 계정 ID 정보
kSecAttrServer as String: server, // 접속하려는 서버 정보
kSecValueData as String: password] // 저장할 아이템의 데이터
query 딕셔너리의 첫 번째 key-value pair는 인터넷 비밀번호를 담는 item을 나타내며 keychain service는 데이터가 비밀이며 암호화가 필요하다고 알려줍니다. 이것은 또한 항목이 적용되는 서버 및 계정과 같은 다른 인터넷 비밀번호와 구별되는 속성을 가지고 있음을 보장합니다. 실제로 쿼리의 다음 두 개의 key-value pair는 이 정보를 제공하며, 사용자로부터 얻은 사용자 이름과 서버로 이 비밀번호에 적합한 도메인 이름을 점부합니다.
키체인 서비스는 또한 관련 kSecClassGenericPassword 항목 클래스를 제공합니다. 일반 비밀번호는 대부분의 면에서 인터넷 비밀번호와 비슷하지만, 원격 액세스와 관련된 특정 속성이 부족합니다(예를 들어, kSecAttrServer 속성이 없습니다). 원격 엑세스를 위한 kSecAttrServer 추가 속성이 필요하지 않을 때, 대신 일반 비밀번호를 사용하세요. 다음과 같은 사용 사례가 있습니다. 사용자 암호 저장, API 키 및 토큰 저장, 일반적인 기밀 정보 저장
이 경우 필요하지는 않지만, 포트 번호나 네트워크 프로토콜과 같은 추가 속성을 지정하여 비밀번호를 더 특성화할 수 있습니다. 예를 들어, 동일한 서버에서 작업하는 동일한 사용자에 대해 별도의 FTP 및 HTTP 자격 증명을 저장해야 하는 경우, kSecAttrProtocol 속성을 추가하여 구별할 수 있습니다.
마지막으로, 쿼리에는 Data 인스턴스로 인코딩된 사용자의 비밀번호가 포함되어 있습니다.
아이템 추가하기
query를 완성하였으면 SecItemAdd(_:_:) 함수에 공급하기만 하면 됩니다
let status = SecItemAdd(query as CFDictionary, nil)
guard status == errSecSuccess else { throw KeychainError.unhandledError(status: status) }
일반적으로 추가 작업의 두 번째 인수에서 참조로 제공된 반환 데이터를 nil로 무시하지만, 항상 함수의 반환 상태를 확인하여 작업이 성공하는지 확인해야합니다. 예를 들어, 주어진 속성을 가진 항목이 이미 존재하는 경우, 작업이 실패할 수 있습니다.
만약 데이터를 반환받고 싶을 때 SecItemAdd(_:_:) 함수는 SecItemCopyMatching(_:_:) 함수와 비슷하게 동작한다는 것을 알고 있어야 합니다.
검색가능하게 보장하기
나중에 해당 항목을 찾기 위해 해당 attributes에 대한 지식을 활용합니다. 이 예에서는 서버와 계정이 항목을 식별하는 특성입니다. 상수 특성(여기서는 서버)의 경우 조회 중에 동일한 값을 사용하세요. 반면에 계정 속성은 동적입니다. 왜냐하면 이는 런타임 중에 사용자가 제공한 값을 가지고 있기 때문입니다. 앱이 동일한 서버에 대한 다양한 계정의 비밀번호와 같은 다양한 특성을 갖는 유사한 항목을 추가하지 않는 한, 동적 속성을 검색 매개변수로서 생략하고 대신 항목과 함께 검색할 수 있습니다. 결과적으로 비밀번호를 조회할 때 해당하는 사용자 이름도 얻게 됩니다.
만약 앱이 동적인 속성이 다른 항목을 추가한다면, 검색 중에 그 중에서 선택할 수 있는 방법이 필요합니다. 하나의 옵션은 항목에 대한 정보를 다른 방식으로 기록하는 것입니다. 예를 들어, 사용자 기록을 Core Data 모델에 유지하는 경우, 비밀번호 필드를 저장한 후에 사용자 이름을 거기에 저장합니다. 나중에 데이터 모델에서 끌어온 사용자 이름을 사용하여 비밀번호를 검색하도록 검색을 조건으로 설정할 수 있습니다.
다른 경우에는 항목을 더 많은 속성으로 세부화하는 것이 의미가 있을 수 있습니다. 예를 들어, 원래 추가 쿼리에 kSecAttrLabel 속성을 포함하여 해당 항목을 특정 목적을 위한 것으로 표시하는 문자열을 제공할 수 있습니다. 그런 다음 나중에 이 속성을 사용하여 검색을 좁힐 수 있습니다.
keychain items 검색
키체인 item은 일치하는 경우 어떤 item 속성을 찾을지와 일치가 발견되면 어떤 내용을 반환할지를 키체인 서비스 API에 알려주는 query 딕셔너리를 사용하여 찾을 수 있습니다. 쿼리 딕셔너리는 검색을 조건을 조정하기 위한 추가적인 매개변수를 지정할 수 있도록 해줍니다. 예를 들어, 문자열 속성을 일치시킬 때 대소문자 구분을 제어하거나 일치하는 item의 수를 제한할 수 있습니다.
검색을 수행하는 예로 저장된 비밀번호 항목을 고려해 봅니다. 네트워크 서비스에 대한 자격 증명을 제공한 후 (앱이 저장하는), 사용자는 잠시 동안 앱을 사용한 다음 다른 작업으로 이동할 수 있습니다. 사용자가 다시 돌아오면 앱은 사용자에게 로그인 화면을 보여주지 않고도 계속 작업하기 위해 서버와 다시 인증해야 할 수 있습니다. 이번에는 앱이 사용자에게 로그인 화면을 표시하지 않고 키체인에서 암호를 로드하여 서버와 자동으로 다시 인증합니다.
검색 query 만들기
검색을 위한 query dictionary를 만듭니다.
let query: [String: Any] = [kSecClass as String: kSecClassInternetPassword,
kSecAttrServer as String: server,
kSecMatchLimit as String: kSecMatchLimitOne,
kSecReturnAttributes as String: true,
kSecReturnData as String: true]
이 query는 Internet password item을 찾는데 찾기 위한 조건을 위와 같이 설정할 수 있습니다. 위에 KSecAttrServer는 password item을 추가했을 때 지정한 server를 사용하며 KSecMatchLimit 파라미터를 사용하여 가져올 값의 수를 제한할 수 있습니다.
마지막으로,KSecReturnAttributes와 KSecReturnData를 true로 설정하여 query는 password attribute에서 attribute과 데이터를 모두 요청합니다. 두 가지를 모두 필요로 하는 이유는 kSecAttrAccount 속성에는 사용자 이름이 포함되어 있고, 항목의 데이터에는 비밀번호 자체가 저장되어 있기 때문입니다.
기본적으로, 앱은 자체 키체인 항목을 자유롭게 검색할 수 있지만 다른 앱의 항목은 검색할 수 없습니다. 그러나 키체인 서비스는 예를 들어 kSecAttrAccessGroup 속성을 사용하여 접근성을 넓히거나 좁히는 메커니즘을 제공합니다.
검색을 시작하기
query 딕셔너리를 만들었다면 SecItemCopyMatching(_:_:) 함수로 검색을 시작합니다.
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) }
많은 Security 프레임워크 함수로 작업을 하기 때문에 먼저 반환받는 status value를 테스트 해봐야합니다. 다른것들과 함께 찾고자 하는 item을 찾지 못했다고 error가 발생할 수 있습니다. 예를들어 이전에 서버에 준 비밀번호를 저장하지 않았을 경우 에러가 발생하는데 이때 errSecItemNotFound 결과를 받게 됩니다. errSecItemNotFound 값으로 상황에 따라 다른 대처를 할 수 있습니다.
만약 검색이 성공하였다면 SecItemCopyMatching(_:_:) 함수로 item 파라미터를 반환할 수 있습니다.
결과를 추출하기
여러 반환 유형을 요청하고 단일 결과(KSecMatchLimitOne)만 허용했기 때문에 검색에서는 결과로 사전(dictionary)을 기대해야 합니다. kSecAttrAccount 키와 관련된 속성 값에서 사용자 이름을 복구합니다. 한편, kSecValueData 키를 사용하여 비밀번호 데이터를 추출하고 이를 문자열로 변환합니다.
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)
만약 검색 시에 kSecMatchLimit 키와 관련된 값으로 1보다 큰 정수를 설정하면, 반환된 항목은 설정한 값으로 제한된 항목 수를 갖는 배열입니다. 이 배열의 각 항목은 일치 항목을 하나로 제한할 때 얻는 벌어진 사전과 같은 형식으로 포맷팅됩니다. 만약 kSecMatchLimit 키에 대해 kSecMatchLimitAll을 지정하면, 배열 크기는 키체인에서 발견된 일치 수에 의해만 제한됩니다.
암호 항목을 복사할 때 kSecReturnData 및 kSecMatchLimitAll 옵션을 결합할 수 없습니다. 왜냐하면 각 암호 항목을 복사하는 데 추가 인증이 필요할 수 있기 때문입니다. 대신 항목에 대한 참조 또는 지속적인 참조를 요청한 다음 실제로 필요한 특정 비밀번호에 대한 데이터만 요청하세요.
결과 배열을 얻으면 관심 있는 단일 항목을 찾기 위해 배열을 반복 검사해야 할 수 있습니다. 이를 위해 계정, 생성 날짜 또는 라벨과 같은 다른 항목 속성을 검사할 수 있습니다. 반면에 미리 구별 특성에 대해 알고 있다면 해당 속성을 사용하여 초기 키체인 검색 쿼리를 좁히는 것이 일반적으로 더 효율적입니다.
여러 일치 항목을 처리해야 하는지 여부는 앱이 키체인을 사용하는 방식에 따라 달라집니다. 사용자가 런타임 중에 여러 식별 정보 중에서 선택할 수 있도록 허용하는 경우, 단일 서버에 대해 여러 암호를 저장하고 사용자에게 저장된 계정을 선택 키로 사용하여 검색 결과 중에서 선택하게 할 수 있습니다. 다른 경우에는 사용자를 참여시키지 않고 검색을 하나의 결과로 필터링할지 여부를 결정할 수 있습니다. 이 접근 방식을 선택하는 경우 키체인에서 유사한 항목을 만들지 않도록 주의하세요. 대신 기존 항목을 테스트하고 삭제하거나 수정하세요.
keychain items 업데이트 및 삭제
keychain items를 업데이트 또는 삭제를 해야할 상황이 있을 수 있습니다. 만약 SecItemAdd(_:_:) 함수를 사용하여 자격 증명을 저장했어도 기존에 이미 저장된 item이 있었다면 검색에 실패할 수 있습니다. 이런 상황에서는 기존에 있던 keychain에 item과 새로운 item은 동시에 저장할 수 없습니다. 이렇게 데이터가 겹쳐서 에러가 발생하는 것을 방지하기 위해서 기존에 있던 데이터를 업데이트를 합니다.
검색을 위한 search querydhk 새로운 attributes 준비
keychain에 저장된 item을 업데이트 하기 위해서는 먼저 업데이트할 대상을 찾아야합니다. 그래서 업데이트를 하기 위해선 SecItemCopyMatching(_:_:) 함수를 통해 찾는것부터 시작합니다.
let query: [String: Any] = [kSecClass as String: kSecClassInternetPassword,
kSecAttrServer as String: server]
이 특정한 query는 server와 연결된 Internet password를 찾는 query입니다. 주의해야할 점은 저장한 비밀번호와 일치해야합니다. 만약 keychain이나 다른 저장소를 사용한다면 찾을 항목에만 영향을 미치도록 이 검색에 더 많은 조건을 부여해야할 수 있습니다. 데이터를 검색하지 않기 때문에 반환 타입을 저장할 필요가 없습니다.
검색 쿼리를 제공하는 것 외에도, 원하는 변경 사항을 설명하기 위해 두 번째 딕셔너리를 제공합니다. kSecValueData 키를 사용하여 저장된 암호를 변경하는 것과 같이 새로운 항목 데이터를 제공할 수 있습니다. 또한 kSecAttrAccount 키를 사용하여 계정을 업데이트하는 것과 같이 항목의 속성을 변경할 수 있습니다.
let account = credentials.username
let password = credentials.password.data(using: String.Encoding.utf8)!
let attributes: [String: Any] = [kSecAttrAccount as String: account,
kSecValueData as String: password]
유저로부터 받은 유저 이름과 비밀번호를 Credentials 인스턴스에 제공할 수 있습니다. 유저의 이름이 기존에 있던 값과 같으면 비밀번호만 변경해도 괜찮습니다.
keychain items 업데이트하기
search query와 새로운 attributes를 사용하여 SetItemUpdate(_:_:) 함수를 호출하면 업데이트를 할 수 있습니다.
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) }
호출에서 반환된 상태를 테스트하고 실패를 처리합니다. 성공하면, 호출는 당신이 제공하는 attributes에 따라 일치하는 모든 항목을 수정합니다.
keychain items 삭제하기
만약 앱에서 서버로부터 로그아웃되어 통화에서 반환된 상태를 테스트하고 실패를 처리하세요. 성공하면, 통화는 당신이 제공하는 속성에 따라 일치하는 모든 항목을 수정합니다. 단지 다른 점은 qeury dictionary를 필요로 합니다. 업데이트를 하기 위해서는 SecItemDelete(_:) 함수를 호출합니다.
let status = SecItemDelete(query as CFDictionary)
guard status == errSecSuccess || status == errSecItemNotFound else { throw KeychainError.unhandledError(status: status) }
기본적으로 keychain service는 검색 매개변수와 일치하는 모든 키체인 item을 삭제합니다. 이미 참조나 지속적인 참조가 있는 특정 item을 삭제하려면 이를 검색 딕셔너리에 kSecMatchItemList 키의 값으로 추가합니다. 이렇게 하면 삭제가 지정된 item에만 제한됩니다.
'SwiftUI' 카테고리의 다른 글
Using Keychain services to save JWT (JSON Web Token) (0) | 2023.11.18 |
---|---|
kSecClass & Item Class Value in Keychain - 키체인 (0) | 2023.11.18 |
Core Data (0) | 2023.11.16 |
EnvironmentObject (0) | 2023.11.15 |
NSCoding (0) | 2023.11.13 |