티스토리 뷰
Keychain
Keychain Service API는 Keychain이라고 부르는 암호화된 데이터베이스 안에 user data를 저장하는 메카니즘을 제공한다.
위에서 보는 것처럼 키체인은 비밀번호로 한정되지 않는다. 신용카드 정보나 짧은 노트들도 저장 가능!
위 뿐 아니라 유저가 평소엔 잘 인지하지 못하지만 일상생활에 필요한 아이템들을 저장할 수 있다. 이렇게 저장한 cryptographic key나 인증서로 다른 유저들과 암호화된 커뮤니케이션을 할 때 신뢰를 받을 수 있다.
Keychain Items
패스워드나 암호화된 키 같이 secret을 저장하고 싶을 때, 그것을 keychain item으로 패키징해야 한다.
아이템의 접근성을 제어하기 위해, 그리고 아이템을 검색할 수 있게 하기 위해 data와 함께, 퍼블릭하게 볼 수 있는 attribute를 제공해야 한다.
키체인 서비스는 keychain 안에서 data encryption과 data attributes를 포함한 storage를 다룬다.
나중에 권한을 부여받은 프로세스는 아이템을 찾고 데이터를 복호화하기 위해 키체인 서비스를 이용한다.
Keychain 아이템을 인터넷 패스워드를 저장하기 위해 사용하는 경우 플로우 차트
첫 실행시 키체인에 패스워드가 없다. 따라서 오른쪽 브랜치를 따라감. 유저가 정보를 제공하면 SecItemAdd
를 통해 키체인에 저장.
SecItemAdd
: autehticated credential을 저장한다.
SecItemCopyMatching
: 패스워드(value)를 찾기 위해 keychain을 검색한다.
SecItemUpdate
:
비밀번호를 바꾸거나 리셋한 경우. → 키체인에 검색하면 이전 패스워드를 전달하기에 authentication이 실패함 !
→ 새로운 credential을 validate한 후 현재 존재하고 있는 저장된 value를 수정한다.
SecItemDelete
: Keychain에서 완전히 password를 지움
Keychain Items 추가 (패스워드 추가)
struct Credentials {
var username: String
var password: String
}
enum KeychainError: Error {
case noPassword
case unexpectedPasswordData
case unhandledError(status: OSStatus)
}
let account = credentials.username // KEY 역할
**let password = credentials.password.data(using: String.Encoding.utf8)! // VALUE 역할 - data 타입이어야..**
// item이 Internet password이며, 데이터가 secret이며 encrytion을 필요로 하다고 keychain service가 추정한다.
// 다른 인터넷 password와 구별하는 attribute를 item이 가지고 있다고 보증한다.
var query: [String: Any] = [kSecClass as String: kSecClassInternetPassword,
kSecAttrAccount as String: account, // 유저로부터 받은 user name을 account에 attach하고
kSecAttrServer as String: server, // domain name은 서버로 attach.
kSecValueData as String: password] // 유저로부터 받은 password를 Data instance로 인코딩하여 가지고 있다.
Note:
Keychain 서비스는kSecClassGenericPassword
아이템 클래스를 또한 제공하는데, 이는 Internet password와 많은 점에서 비슷하지만 remote access에 대한 특정한 애트리뷰트가 없다. (ex. kSecAttrServer가 없다.) 만약 extra attribute가 필요 없다면 generic password를 쓸 것
kSecAttrProtocol
을 이용하면 포트 넘버나 네트워크 프로토콜과 같이 Additional attribute를 구체화하여 패스워드를 구체화할 수 있다.
쿼리가 끝나면 SecItemAdd
를 적용한다.
let status = SecItemAdd(query as CFDictionary, nil)
guard status == errSecSuccess else { throw KeychainError.unhandledError(status: status) }
항상 function의 return status를 확인할 것! 주어진 attribute에 대해 아이템이 이미 있을 수 있을 경우 operation이 fail할 수 있다.
Keychain Items 검색하기
검색 쿼리 만들기
let query: [String: Any] = [kSecClass as String: kSecClassInternetPassword,
kSecAttrServer as String: server, // 일종의 키의 역할
kSecMatchLimit as String: kSecMatchLimitOne, // result를 하나의 value로 한정짓는다 -(기본값)
kSecReturnAttributes as String: true, // 쿼리는 password item으로부터 그것의 attributes와 data를 둘 다 요청한다.
kSecReturnData as String: true]
검색 초기화
var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &**item**)
guard status != errSecItemNotFound else { throw KeychainError.noPassword } // 이전에 password를 server에 대해 저장한 적이 없을 때.
guard status == errSecSuccess else { throw KeychainError.unhandledError(status: status) }
결과 추출
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)
Update
Search Query 와 New Attributes 가 필요합니다.
키체인 아이템을 Update하기 위해선 일단 아이템을 찾아야 한다. SecItemCopyMatching 함수에서 했던 것처럼
let **query**: [String: Any] = [kSecClass as String: kSecClassInternetPassword,
kSecAttrServer as String: **server**]
두번째 딕셔너리에는 desired change를 기술한다.
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]
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) }
Delete
let status = SecItemDelete(query as CFDictionary)
guard status == errSecSuccess || status == errSecItemNotFound else { throw KeychainError.unhandledError(status: status) }
사용 예
! 개선이 필요할 수 있음.
! 위의 kSecClassInternetPassword
와 달리 kSecClassGenericPassword
이용
! RxSwift 간단히 적용
//
// ViewController.swift
// KeychainPractice
//
// Created by YOONJONG on 2022/11/14.
//
import UIKit
import RxSwift
enum KeychainError: Error {
case noPassword
case duplicatedKey
case unexpectedPasswordData
case unexpectedToken
case unhandledError(status: OSStatus)
}
class ViewController: UIViewController {
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
createToken(key: "yoonjong1820@gmail.com", token: "a1s2d3d4f5g")
.subscribe { event in
switch event {
case .success(let value):
print("createToken : ", value)
case .failure(let error):
print("createTokenError: ", error)
}
}
.disposed(by: disposeBag)
getToken(key: "yoonjong1820@gmail.com")
.subscribe { event in
switch event {
case .success(let value):
print("getToken : ", value)
case .failure(let error):
print("getTokenError: ", error)
}
}
.disposed(by: disposeBag)
print(updateToken(key: "yoonjong1820@gmail.com", token: "1234"))
deleteToken(key: "yoonjong1820@gmail.com")
.subscribe { event in
switch event {
case .success(let value):
print("deleteToken : ", value)
case .failure(let error):
print("deleteTokenError: ", error)
}
}
.disposed(by: disposeBag)
}
func createToken(key: Any, token: Any) -> Single<Bool> {
return Single.create { single in
print("=== Create Token ===")
let token = (token as AnyObject).data(using: String.Encoding.utf8.rawValue)!
let addQuery: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecValueData as String: token
]
let status = SecItemAdd(addQuery as CFDictionary, nil)
if status == errSecSuccess {
single(.success(true))
}
else if status == errSecDuplicateItem {
single(.failure(KeychainError.duplicatedKey))
}
else {
single(.failure(KeychainError.unhandledError(status: status)))
}
return Disposables.create()
}
}
func getToken(key: Any) -> Single<Any> {
print("=== getToken ===")
return Single.create { single in
let getQuery: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecReturnAttributes as String: true,
kSecReturnData as String: true
]
var item: CFTypeRef?
let result = SecItemCopyMatching(getQuery as CFDictionary, &item)
if result == errSecSuccess {
if let existingItem = item as? [String: Any],
let data = existingItem[kSecValueData as String] as? Data,
let token = String(data: data, encoding: .utf8) {
single(.success(token))
} else {
single(.failure(KeychainError.unexpectedToken))
}
} else {
single(.failure(KeychainError.unexpectedToken))
}
return Disposables.create()
}
}
func updateToken(key: Any, token: Any) -> Bool {
print("=== updateToken ===")
let prevQuery: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key
]
let token = (token as AnyObject).data(using: String.Encoding.utf8.rawValue)!
let updateQuery: [String: Any] = [
kSecValueData as String: token as Any
]
let status = SecItemUpdate(prevQuery as CFDictionary, updateQuery as CFDictionary)
if status == errSecSuccess {
return true
} else {
return false
}
}
func deleteToken(key: Any) -> Single<Bool> {
print("=== deleteToken ===")
return Single.create { single in
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key
]
let status = SecItemDelete(query as CFDictionary)
if status == errSecSuccess {
single(.success(true))
} else {
single(.failure(KeychainError.unhandledError(status: status)))
}
return Disposables.create()
}
}
}
참고자료
'Swift > Swift' 카테고리의 다른 글
클린 아키텍처 구조에서 유즈케이스 테스트하기 (0) | 2022.12.15 |
---|---|
Swift Realm 시작하기 (0) | 2022.03.10 |
Swift Alamofire 시작하기 (0) | 2022.03.10 |
Swift 알라딘 베스트셀러 크롤링하기(SwiftSoup) (0) | 2022.03.10 |
IntrinsicContentSize - Content hugging과 Compression resistance의 개념 (0) | 2021.12.02 |
- Total
- Today
- Yesterday
- BeautifulSoup
- 스위프트
- IOS
- dismiss
- 2024년
- swift
- IntrinsicContentSize
- XCTest
- 오토레이아웃
- 유즈케이스
- Realm
- UITest
- 2023년
- equaltosuperview
- collectionViewCell
- CRAWL
- 회고
- http/1.1
- Info.plist
- snapkit
- 클린 아키텍처
- 웹모바일
- Clean Architecture
- Autolayout
- Kotlin
- CollectionView
- KeyChain
- http/1
- 부스트캠프
- 네트워킹
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |