Adding a Password to the Keychain | Apple Developer Documentation
<aside> 💡 키체인이 필요한 이유
accessToken 과 refreshToken 을 저장해 둘 곳이 필요하기 때문 ! 다만 UserDefaults 를 사용하기에는 보안이 아쉬움 …
</aside>
로그인하면 서버에서 헤더로 accessToken 이랑 refreshToken 을 내려주는데,
지금은 아래 코드를 통해 UserDefaults 에 저장해서 사용 중
func onSucceed(_ response: Response, target: TargetType, isFromError: Bool) {
let request = response.request
let url = request?.url?.absoluteString ?? "nil"
let statusCode = response.statusCode
var log = "------------------- 네트워크 통신 성공 -------------------"
log.append("\\n[\\(statusCode)] \\(url)\\n----------------------------------------------------\\n")
if let authorization = response.response?.value(forHTTPHeaderField: "Authorization") {
UserDefaults.standard.setValue(authorization, forKey: "Authorization")
print("❤️🔥authorization:\\(authorization)")
}
if let refresh = response.response?.value(forHTTPHeaderField: "Authorization-refresh") {
UserDefaults.standard.setValue(refresh, forKey: "refresh")
print("❤️🔥autorization-refresh:\\(refresh)")
}
}
여기에서 KeyChain 에 저장해둬야 함
먼저 오류 발생 시 처리해 줄 KeychainError Enum 생성
enum KeychainError: Error {
case noData
case unexpectedData
case unexpectedError(status: OSStatus)
}
그리고 Key 값에 접근을 좀 더 쉽게 하기 위해 KeychainKey Enum 생성
enum KeychainKey {
case access
case refresh
case accessRefreshPair
}
accessRefreshPair 는 둘 다 한꺼번에 갱신하거나 사용해야 할 때 ( 가능한가 ? )
그 후 저장할 데이터를 [키체인 속성: 데이터] 형태의 딕셔너리로 만들어서 쿼리 생성 후 데이터 저장
지금 건빵에서는 accessToken 과 refreshToken 을 저장할 것이기 때문에,
kSecClass 값으로 kSecClassGenericPassword 를 선택했는데 …
사실 자세히 알아서 했다기보다는 일단은 제일 보편적으로 쓰이는걸로 선택 !
그렇게 선택한 query 의 키 값들은 아래와 같음
kSecAttrService : 해당 키체인이 사용되는 서비스 이름kSecAttrLabel : 해당 키체인의 태그같은 느낌 ?kSecValueData : 해당 키체인의 실질적인 데이터let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: serviceName,
kSecAttrLabel as String: key.label,
kSecValueData as String: value.data(using: .utf8) as Any
]
createKeychain()static func createKeychain(of key: KeychainKey, with value: String) {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: serviceName,
kSecAttrLabel as String: key.label,
kSecValueData as String: value.data(using: .utf8) as Any
]
let status = SecItemAdd(query as CFDictionary, nil)
#if DEBUG
switch status {
case errSecSuccess:
print("🔒 Keychain created successfully 🔒")
case errSecDuplicateItem:
print("❌ Keychain item already exists ❌")
default:
print("❌ Unknown error: \\(SecCopyErrorMessageString(status, nil).debugDescription) ❌")
}
#endif
}
query 구성은 위에서 설명한 바와 같이 진행
그 후 status 라는 변수에 SecItemAdd 로 keychain 추가 후 결과인 OSStatus 타입을 저장해두고,
디버깅 시 OSStatus 결과에 따라 print 문을 통해 표시해두는 코드도 추가해 뒀다
readKeychain()static func readKeychain(of key: KeychainKey) -> String {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: serviceName,
kSecAttrLabel as String: key.label,
kSecMatchLimit as String: kSecMatchLimitOne,
kSecReturnData as String: true,
kSecReturnAttributes as String: true
]
var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &item)
#if DEBUG
switch status {
case errSecSuccess:
print("🔒 Keychain read successfully 🔒")
case errSecItemNotFound:
print("❌ Keychain item not found ❌")
return ""
default:
print("❌ Unknown error: \\(SecCopyErrorMessageString(status, nil).debugDescription) ❌")
return ""
}
#endif
guard let decodedItem = item as? [String: Any],
let tokenData = decodedItem[kSecValueData as String] as? Data,
let token = String(data: tokenData, encoding: .utf8)
else {
print("❌ Keychain decoding failed ❌")
return ""
}
return token
}
여기도 query 구성은 비슷한데, 추가된 부분이 몇 가지 있음
kSecMatchLimit : query 날린 것 중 같은 키체인이 여러개 있을 때 어떻게 처리할지 설정kSecReturnData : 데이터를 가져다 줄지 설정kSecReturnAttributes : 키체인의 attribute 도 함께 반환할건지 설정그 후 똑같이 #if DEBUG ~ #endif 문으로 키체인 사용 시 로그 찍어두었음
그리고 지금은 읽는 과정이고, 그 과정에서 디코딩도 필요하기 때문에, 디코딩 과정이 추가된다
<aside>
🤔 여기 구성하면서 고민하던 부분이, 버그가 발생했을 때 switch - case 문으로
status 에 따라 return “” 를 해뒀는데, 원래는 nil 값을 반환하려다가
키체인을 사용하는 지점에서 KeychainService.readKeychain() 을 실행시키면
올바른 토큰이 내려오더라도 Optional(”~~”) 로 내려오게 돼서
적절한 통신이 되지 않는 경우가 발생했다 ….
그래서 요 부분 어떻게 할지 고민 중 ….
</aside>
updateKeychain()키체인이 좀 킹받는 부분 중 하나가, 이미 동일 키체인이 있으면 SecItemAdd() 를 했을 때
기존 것 위에 덮어씌워지는게 아니라, 중복된 값이라며 저장을 못하게 한다
그래서 키체인을 업데이트 하는 과정이 필요한데,
그건 SecItemUpdate() 으로 구현한다
static func updateKeychain(of key: KeychainKey, to value: String) {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword
]
let attributes: [String: Any] = [
kSecAttrService as String: serviceName,
kSecAttrLabel as String: key.label,
kSecValueData as String: value.data(using: .utf8) as Any
]
let status = SecItemUpdate(query as CFDictionary, attributes as CFDictionary)
#if DEBUG
switch status {
case errSecSuccess:
print("🔒 Keychain updated successfully 🔒")
case errSecItemNotFound:
print("❌ Keychain item not found ❌")
default:
print("❌ Unknown error: \\(SecCopyErrorMessageString(status, nil).debugDescription) ❌")
}
#endif
}
코드는 이전의 메소드들과 비슷비슷 ~~
deleteKeychain()