본문 바로가기

TIL

[TIL] Keychain 적용기

적용 배경

이전부터 유저 정보 저장용으로 계속 UserDefaults만 사용했기 때문에 이번에 보안성 좀 더 강하다는 Keychain을 적용해보려고 합니다.

 

Keychain에 대하여

등장배경

 

Secure login with iCloud Keychain verification codes - WWDC21 - Videos - Apple Developer

Learn how you can support on-device verification codes in your app or website for a more secure sign-in experience. We'll explore the...

developer.apple.com

 

WWDC21 영상에 의하면 많은 유저들이 여러 디바이스에 걸쳐 동일한 비밀번호를 보유 및 사용하여 개인정보 보안에 있어 취약한 상황에 놓여있다고 합니다. 따라서 이를 해결할 일반적인 방법으로 유저에 대한 추가적인 본인 인증 과정을 거치게 한다고 합니다.

 

이 추가적인 인증 방법에는 Verification Code를 발행하는 것인데요.

이에 크게 SMS 방식과 TOPT(Time-based One-time Password)이 있다고 합니다.

각 방식에 대해 간단히 살펴보겠습니다.

SMS

기본적으로 Verification Code를 문자형태로 수신하는 방식입니다.

 

이 방식의 장점으로는 한번만 사용(Single Use)된다는 점이지만 Snooping, SIM-swapping, 문자 전송간 손실, 추가적인 비용 발생 등의 단점을 가지고 있습니다.

Time-based One-time Password(TOTP)

On-Device 형태로 시간을 기반으로 인증 코드를 생성해주는 방식입니다.

 

이 방식의 장점으로는 SMS의 단점 중 하나인 비용이 발생하지 않는다는 것입니다. 

또 다른 장점으로 디바이스 자체 내부적으로 인증 코드를 생성하기 때문에 보안성이 뛰어나다는 장점이 있습니다.

 

허나 이 방식 또한 완벽하진 않다고 합니다. 왜냐하면 앱을 설치하고 어떠한 인터페이스를 통하여 유저에게 SecretKey를 공유하는 등 초기 설정이 조금 번접한 면이 있다고 합니다. 이때 유저에게 SecretKey를 전달하는 방식으론 주로 QR code를 활용한다고 합니다.

 

위의 이러한 SMS와 TOPT의 단점을 극복하고자 iCloud Keychain이라는 방식 AutoFill 기능과 함께 등장하게 되었다고 합니다.

 

Keychain이란

위에 Keychain에 대한 등장배경에서 언급했듯 안전하지 않은 방법으로 사용되는 개인정보들에 대해 좀더 암호화된(encrypted) 방식으로 유저 개인정보를 처리하기 위해 등장하였습니다. 여기서 개인정보라 함은 비밀번호, 개인 금융 정보, 토큰과 같은 암호화 키 및 인증서 등을 일컫습니다.

 

Keychain은 저장될 때 keychain item으로 패키지화되어 저장된다고 합니다. 

 

아래 그림은은 Apple 공식 사이트에서 가져온 그림인데요.

하단 그림을 보시면 Attribute와 Data 형태로 Keychain Service API를 통해 Keychain Item으로 변환된 다음 Keychain Storage에 저장되는다는 내용입니다. 이때 Data는 암호화된 형태로 저장된다고 아래 그림에서는 나태내고 있습니다.

 

 

활용법

역시나 Apple 공식문서에 의하면 아래 Keychain 관리는 아래와 같은 그림의 흐름대로 고려해보는 것을 추천한다고 합니다.

이를 토대로 실제로 토큰을 저장하는 코드를 구현해보도록 하겠습니다.

Keychain에 추가

static func create(key: Key, value: String) {
    // 데이터 추가를 위한 쿼리 생성
    let query: NSDictionary = [
        kSecClass: kSecClassGenericPassword,
        kSecAttrAccount: key.rawValue,
        kSecValueData: value.data(using: .utf8, allowLossyConversion: false) as Any
    ]
    
    // 중복 데이터 제거
    SecItemDelete(query)

    // 데이터 추가
    let status = SecItemAdd(query, nil)
    assert(status == noErr, "failed to save Token")
}

 

쿼리 생성

let query: NSDictionary = [
    kSecClass: kSecClassGenericPassword,
    kSecAttrAccount: key.rawValue,
    kSecValueData: value.data(using: .utf8, allowLossyConversion: false) as Any
]

 

처음 데이터를 저장하기 위해 쿼리를 생성합니다.

각 Dictionary의 key-value pair에 대해 간단히 살펴보겠습니다.

  • 첫번째 key-value: kSecClass는 Keychain Item의 클래스가 무엇인지 즉, Keychain Item이 어떤 종류인지를 나타냅니다.
    • 위 코드를 예로 들자면 kSecClassInternetPasswordKeychain Item이 Generic password(일반적인 비밀번호)임을 나타냅니다.
    • 이 말은 Keychain service가 이것을 보고, Data가 기밀 정보이고, 암호화가 필요로 한다고 추론합니다.
  • 두번째 key-value:  kSecAttrAccount는 보통 Keychain Item을 구별해주는 용도로 사용됩니다.
    • 주로 유저 이름, 이메일 등 identifier로서 구분자가 되어주는 데이터를 저장됩니다.
    • 여기서는 유저로부터 받아온 키 값을 저장합니다.
  • 마지막 key-value: kSecValueDataKeychain Storage에 암호화 저장되는 실제 값을 의미합니다.

데이터 추가

let status = SecItemAdd(query, nil)
assert(status == noErr, "failed to save Token")

SecItemAdd(_:_:) 메서드를 통해 데이터를 Keychain Storage에 저장합니다.

 

Keychain에서 데이터 조회

static func read(key: Key) -> String? {
    // 저장된 데이터 호출을 위한 쿼리 생성
    let query: NSDictionary = [
        kSecClass: kSecClassGenericPassword,
        kSecAttrAccount: key.rawValue,
        kSecReturnData: kCFBooleanTrue as Any,
        kSecMatchLimit: kSecMatchLimitOne
    ]

    // 데이터 조회
    var dataTypeRef: AnyObject?
    let status = SecItemCopyMatching(query, &dataTypeRef)

    // 데이터 조회 결과 처리
    if status == errSecSuccess {
        if let retrievedData: Data = dataTypeRef as? Data {
            let value = String(data: retrievedData, encoding: String.Encoding.utf8)
            return value
        } else { return nil }
    } else {
        print("failed to loading, status code = \(status)")
        return nil
    }
}

쿼리 생성

let query: NSDictionary = [
    kSecClass: kSecClassGenericPassword,
    kSecAttrAccount: key.rawValue,
    kSecReturnData: kCFBooleanTrue as Any,
    kSecMatchLimit: kSecMatchLimitOne 
]

 

 

검색을 위한 쿼리를 생성해줍니다.

위에서 설명 Keychain 추가에서와 마찬가지로 각 Dictionary의 key-value pair에 대해 간단히 살펴보겠습니다.

  • 첫번째 key-value: 위에 언급했듯 kSecClassKeychain Item이 무슨 종류의 클래스인지 나타내줍니다.
  • 두번째 key-value: kSecAttrAccount검색할 데이터의 identifier를 나타냅니다.
  • 세번째 key-value: kSecReturnData쿼리와 일치하는 Keychain Item이 검색되면 그 Item을 반환하라는 나타내는 Attribute로, kCFBooleanTrue는 반환 타입을 CFData 타입으로 반환해야함을 의미합니다.
  • 마지막 key-value: kSecMatchLimit쿼리와 일치된 Keychain Item의 개수 제한을 나타내며, kSecMatchLimitOne검색 시 일치하는 항목을 하나만 반환하도록 해주는 상수입니다.

데이터 조회

var dataTypeRef: AnyObject?
let status = SecItemCopyMatching(query, &dataTypeRef)

 

SecItemCopyMatching(_:_:)  메서드를 통해 쿼리와 일치하는 Keychain Item의 데이터를 dataTypeRef에 반환합니다.

데이터 조회 결과 처리

if status == errSecSuccess {
    if let retrievedData: Data = dataTypeRef as? Data {
        let value = String(data: retrievedData, encoding: String.Encoding.utf8)
        return value
    } else { return nil }
} else {
    print("failed to loading, status code = \(status)")
    return nil
}

 

데이터 조회 결과로 성공하면, dataTypeRef에 조회된 결과가 있는지 확인하고 있다면 외부로 반환해줍니다.

 

Keychain 삭제

static func delete(key: Key) {
    // 데이터 삭제를 위한 쿼리 생성
    let query: NSDictionary = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrAccount as String: key.rawValue
    ]
    
    // 일치하는 데이터 삭제
    let status = SecItemDelete(query)
    assert(status == noErr, "failed to delete the value, status code = \(status)")
}

쿼리 생성

let query: NSDictionary = [
    kSecClass as String: kSecClassGenericPassword,
    kSecAttrAccount as String: key.rawValue
]

 

앞서 Keychain 추가와 삭제에서 작성한 것처럼 마찬가지로 삭제하기 위한 쿼리를 생성합니다.

(여기선 각 key-value에 대한 설명은 생략하도록 하겠습니다.)

일치하는 데이터 삭제

let status = SecItemDelete(query)
assert(status == noErr, "failed to delete the value, status code = \(status)")

 

SecItemDelete(_:) 메서드를 통해 쿼리와 일치하는 데이터를 Keychain Storage에서 삭제해줍니다.

 

마무리

Keychain 정리.. 정말 쉽지 않은 정리였네요.

오늘도 저의 포스팅을 읽어주셔서 감사합니다!

 

참고 사이트

https://developer.apple.com/videos/play/wwdc2021/10105/ (WWDC21 - Keychains)

https://mini-min-dev.tistory.com/114 (실제 구현 참고)

https://applecider2020.tistory.com/48 (Keychain 공식문서 정리)

https://green1229.tistory.com/56 (참고)

https://developer.apple.com/documentation/security/keychain_services/keychain_items (애플 공식문서 - Keychain items)

https://developer.apple.com/documentation/security/keychain_services/keychain_items/using_the_keychain_to_manage_user_secrets (애플 공식문서 - Using the keychain to manage user secrets)

https://developer.apple.com/documentation/security/keychain_services/keychain_items/adding_a_password_to_the_keychain (애플 공식문서 - Adding a password to the keychain)

https://developer.apple.com/documentation/security/keychain_services/keychain_items/searching_for_keychain_items (애플 공식문서 - Searching for keychain items)

https://developer.apple.com/documentation/security/keychain_services/keychain_items/updating_and_deleting_keychain_items (애플 공식문서 - Updating and deleting keychain items)