티스토리 뷰

Keychain

Keychain Service API는 Keychain이라고 부르는 암호화된 데이터베이스 안에 user data를 저장하는 메카니즘을 제공한다.

Untitled

위에서 보는 것처럼 키체인은 비밀번호로 한정되지 않는다. 신용카드 정보나 짧은 노트들도 저장 가능!

위 뿐 아니라 유저가 평소엔 잘 인지하지 못하지만 일상생활에 필요한 아이템들을 저장할 수 있다. 이렇게 저장한 cryptographic key나 인증서로 다른 유저들과 암호화된 커뮤니케이션을 할 때 신뢰를 받을 수 있다.

Keychain Items

패스워드나 암호화된 키 같이 secret을 저장하고 싶을 때, 그것을 keychain item으로 패키징해야 한다.

아이템의 접근성을 제어하기 위해, 그리고 아이템을 검색할 수 있게 하기 위해 data와 함께, 퍼블릭하게 볼 수 있는 attribute를 제공해야 한다.

Untitled

키체인 서비스는 keychain 안에서 data encryption과 data attributes를 포함한 storage를 다룬다.

나중에 권한을 부여받은 프로세스는 아이템을 찾고 데이터를 복호화하기 위해 키체인 서비스를 이용한다.

 

 

Keychain 아이템을 인터넷 패스워드를 저장하기 위해 사용하는 경우 플로우 차트

Untitled

 

첫 실행시 키체인에 패스워드가 없다. 따라서 오른쪽 브랜치를 따라감. 유저가 정보를 제공하면 SecItemAdd 를 통해 키체인에 저장.

SecItemAdd : autehticated credential을 저장한다.

Apple Developer Documentation

 

SecItemCopyMatching : 패스워드(value)를 찾기 위해 keychain을 검색한다.

Apple Developer Documentation

 

SecItemUpdate :

비밀번호를 바꾸거나 리셋한 경우. → 키체인에 검색하면 이전 패스워드를 전달하기에 authentication이 실패함 !

→ 새로운 credential을 validate한 후 현재 존재하고 있는 저장된 value를 수정한다.

 

SecItemDelete : Keychain에서 완전히 password를 지움

Apple Developer Documentation

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()
        }
    }
}

참고자료

https://dvlpr-chan.tistory.com/27

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/06   »
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 29
30
글 보관함