서버로부터 받은 토큰을 저장을 해야할 상황이 있습니다. 그러기 위해서는 해당 토큰을 UserDefault, @AppStorage 와 같은 접근이 쉬운 곳에 값을 저장하기보다 보안적으로 안전하게 저장을 해야하는데 방법으로는 Keychain service를 이용하는 방법이 있습니다.
Keychain이란?
Apple이 제공하는 보안 프레임워크로 암호화된 데이터베이스에 작은 사용자 데이터를 저장할 수 메커니즘
Keychain에 더 자세히 알고 싶으면 여기를 클릭하세요
Keychain에는 간단한 데이터를 안전하게 저장할 수 있습니다. 예를 들어 다음과 같은 데이터가 있습니다.
struct Credentials: Codable {
var username: String
var psssword: String
var token: String
}
다음으로는 error enumeration을 정의하여 keychain으로부터 받는 결과를 확인할 수 있습니다.
enum KeychainError: Error {
case noPassword
case unexpectedPasswordData
case unhandledError(status: OSStatus)
case notFound
case alreadyExist
}
그런 다음 토큰을 요청할 서버를 정의하는 URL 상수를 만듭니다.
static let server = "서버 주소"
키체인에 토큰 저장하기
위에서 정의한 Credentials 데이터를 저장하기 위해서는 다음과 같은 저장하는 함수를 만들어야합니다. kSecClass를 kSecClassInternetPassword로 인터넷에서 받은 중요한 데이터를 저장하는 유형으로 만듭니다. 이후 kSecAttrServer로 데이터를 제공한 서버의 정보(ex. URL)를 저장합니다. 다음으로 kSecAttrAccount로 계정 정보를 저장하고 kSecValueData로 저장할 데이터를 받도록 합니다.
여기서 포인트는 kSecValueData는 CFData 타입이기 때문에 data 타입으로된 데이터를 저장해야합니다. 아래에서는 kSecValueData의 value 값으로 저장할 데이터를 JSONEncoder로 Encode하여 여러개의 데이터를 한번에 저장할 수 있도록 하였습니다.
static func save(credentials: Credentials) throws {
let encoder = JSONEncoder()
let data = try encoder.encode(credentials)
let query: [String: Any] = [kSecClass as String: kSecClassInternetPassword, // keychain Item Class
kSecAttrServer as String: server, // 접속하려는 서버 정보
kSecAttrAccount as String: credentials.username, // 계정 ID 정보
kSecValueData as String: data]
let status = SecItemAdd(query as CFDictionary, nil) // 키체인에 항목 추가하기
if status == errSecSuccess {
print("Successfully added to keychain.")
} else {
if let error: String = SecCopyErrorMessageString(status, nil) as String? {
print(error, status.hashValue)
}
}
}
키체인에 저장한 토큰 가져오기
static func get() throws -> Credentials {
let query: [String: Any] = [kSecClass as String: kSecClassInternetPassword,
kSecMatchLimit as String: kSecMatchLimitOne, // 중복된 데이터가 있을경우 하나만 반환
kSecAttrServer as String: server, // server로 접근할 데이터 검색
kSecReturnAttributes as String: true, // username 데이터를 반환
kSecReturnData as String: true] // encode된 Credentials 데이터를 반환
var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &item)
guard status != errSecItemNotFound else { throw KeychainError.notFound }
guard status == errSecSuccess else { throw KeychainError.unhandledError(status: status) }
let decoder = JSONDecoder()
guard let existingItem = item as? [String : Any] else {
throw KeychainError.unexpectedPasswordData
}
let decoded = try decoder.decode(Credentials.self, from: (existingItem[kSecValueData as String] as? Data)!)
return Credentials(username: decoded.username, psssword: decoded.psssword, token: decoded.token)
}
키체인에 저장된 토큰 업데이트하기
static func update(credentials: Credentials) throws {
let query: [String: Any] = [kSecClass as String: kSecClassInternetPassword,
kSecAttrServer as String: server]
let encoder = JSONEncoder()
let data = try encoder.encode(credentials)
let attributes: [String: Any] = [kSecAttrAccount as String: credentials.username,
kSecValueData as String: data]
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) }
}
키체인에 저장된 토큰 삭제하기
static func delete() throws {
let query: [String: Any] = [kSecClass as String: kSecClassInternetPassword,
kSecAttrServer as String: server]
let status = SecItemDelete(query as CFDictionary)
if status == errSecSuccess {
print("Successfully deleted data from keychain.")
} else {
if let error: String = SecCopyErrorMessageString(status, nil) as String? {
print(error)
}
}
guard status == errSecSuccess || status == errSecItemNotFound else { throw KeychainError.unhandledError(status: status) }
}
키체인에 저장된 토큰 유효한지 확인하기
import JWTDecode
static func CheckToken() throws -> Bool {
let query: [String: Any] = [kSecClass as String: kSecClassInternetPassword,
kSecMatchLimit as String: kSecMatchLimitOne,
kSecAttrServer as String: server,
kSecReturnAttributes as String: true,
kSecReturnData as String: true]
var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &item)
guard status != errSecItemNotFound else {
throw KeychainError.notFound
}
guard status == errSecSuccess else { throw
KeychainError.unhandledError(status: status)
}
let decoder = JSONDecoder()
guard let existingItem = item as? [String : Any] else {
throw KeychainError.unexpectedPasswordData
}
let decoded = try decoder.decode(Credentials.self, from: (existingItem[kSecValueData as String] as? Data)!)
let token = try JWTDecode.decode(jwt: decoded.token)
return token.expired
}
'SwiftUI' 카테고리의 다른 글
Preventing Insecure Network Connections - 네트워크 보안 (0) | 2023.11.18 |
---|---|
Security (0) | 2023.11.18 |
kSecClass & Item Class Value in Keychain - 키체인 (0) | 2023.11.18 |
Keychain services (0) | 2023.11.16 |
Core Data (0) | 2023.11.16 |