본문 바로가기

Swift

생체인증을 통한 본인인증 제작기

이전 '보안 핀코드 입력화면 제작기'에 이어서 이번에는 생체인증을 통한 본인인증 진행을 위해 구현한 부분들을 설명해볼까 합니다.

 

 

요구사항

1. 기기별로 탑재된 생체인증 기능을 활용하여 본인여부를 판단

2. 생체인증 진행 및 발생한 오류 및 실패 처리

3. 생체인증 정보 변경 시 앱에서 재활성화 유도

 

위 세가지 요구사항을 순차적으로 설명드리도록 하겠습니다.

 

0. 구조설계

구현 방식을 설명 드리기에 앞서 해당 기능 구현을 위해 설계한 구조에 대해 먼저 설명드리겠습니다.

 

앞서 설명드렸듯이 현재 진행중인 프로젝트는 ReactorKit을 사용 중이고,

 

따라서 생체 인증을 요청 및 처리하는 방식은 RxSwift를 사용하여 처리하였습니다.

 

1. 기기 생체인증(TouchID, FaceID) 사용 가능 여부 확인

2. 생체인증 사용 가능 기기의 경우 PIN 번호 인증 진행

3. PIN 번호 인증 성공 시 생체인증 진행

4. 생체인증 성공 시 앱 생체인증 기능 활성화

 

다음과 같은 프로세스로 설계하였으며,

 

앞서 '보안 핀코드 입력화면 제작기' 에서 핀코드 인증에 대하여 설명 드렸기에, 

 

생체인증 구현 방식에 초점을 맞추어 설명드리겠습니다.

 

 

 

우선 생체인증 기능을 사용하기 위해 Apple의 LocalAuthentication 프레임워크를 사용하였습니다.

 

해당 프레임워크의 깊은 이해를 원하시는 분은 아래 공식 문서를 확인하시면 되겠습니다.

 

https://developer.apple.com/documentation/localauthentication/

 

Apple Developer Documentation

 

developer.apple.com

 

1.  기기 생체인증 상태 확인

 

우선 생체인증 상태가 확인 된 이후 리턴될 타입에 대하여 정의하였습니다.

 

현재 생체인증 기능을 탑재한 Apple의 iOS / iPadOS 기기의 경우 TouchID와 FaceID 둘 중 하나의 기능만을 제공합니다.

 

하지만, 최근 들려오는 신제품에 따라 두 가지 기능을 모두 제공할 수 있다는 가능성을 염두해 두고 코드를 작성하였습니다.

 

따라서 아래와 같이

 

- 생체인증을 지원하지 않는 기기

- Touch ID 

- Face ID

- 특정 이유에 의해 생체인증이 비활성화 된 경우

 

로 구분되는 enum 타입을 생성하였습니다.

 

enum BiometricType {
    case NONE
    case TOUCH_ID
    case FACE_ID
    case NOT_AVAILABLE(reason: String)
    
    var isAvailable: Bool {
        switch self {
        case .FACE_ID, .TOUCH_ID:
            return true
        default:
            return false
        }
    }
}

 

기기의 생체인증 상태를 확인한 이후 위 enum 타입을 리턴함으로서 

 

기기의 상태값에 따른 처리가 용이했습니다.

 

class func getBioStatus() -> Observable<BiometricType> {
	Observable.create { observer in
    	let authContext = LAContext()
        var authorizationError: NSError?
        let _ = authContext.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics,
                                              error: &authorizationError)
        if let error = authorizationError {
            //생체인증이 기능을 지원하지만 사용할 수 없는 경우
            observer.onError(error)
            observer.onCompleted()  
        } else {
            switch(authContext.biometryType) {
            case .none:
                observer.onNext(.NONE)
                observer.onCompleted()
                    
            case .touchID:
                observer.onNext(.TOUCH_ID)
                observer.onCompleted()
                    
            case .faceID:
                observer.onNext(.FACE_ID)
                observer.onCompleted()
                    
            @unknown default:
                observer.onNext(.NONE)
                observer.onCompleted()
            }
        }
        return Disposables.create()
    }
}

위 코드를 통해 현재 기기의 생체 컨텍스트 상태 값을 확인한 이후 Observable<BiometricType>을 리턴합니다.

 

2. 생체인증 진행 및 오류 처리

생체인증을 진행할 수 있는 상태로 확인되었다면, 실제로 생체인증을 진행합니다.

 

생체인증을 요청하는 방법은 매우 간단합니다.

 

LAContext로 부터 지정된 Policy에 따라 생체인증을 진행하고, 결과값을

func evaluatePolicy(_ policy: LAPolicy, localizedReason: String, 
					reply: @escaping (Bool, Error?) -> Void)

(성공 여부, 옵셔널 에러) 튜플 형식을 포함한 클로저를 리턴해줍니다.

 

해당 프로젝트에서는  Rx 형식으로 코드를 처리하기 때문에 해당 함수를

static func evaluatePolicy(context: LAContext) -> Observable<Bool> {
    return Observable.create { observer in
        context.localizedFallbackTitle = String.empty
        context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics,
                               localizedReason: "생체 정보를 확인합니다") { success, error in
            if let error = error {
            	// LAError
                observer.onError(error)
            }
            
            observer.onNext(success)
            observer.onCompleted()
        }
        
        return Disposables.create()
    }
}

다음과 같은 형태로 사용합니다.

 

위 코드에서 주의하실 점은

 

func evaluatePolicy(_ policy: LAPolicy, localizedReason: String, replay: @escaping (Bool, Error?) -> Void)

 

의 LAPoilcy가  .deviceOwnerAuthenticationWithBiometrics 

 

로 설정되어 있는 부분입니다.

deviceOwnerAuthenticationWithBiometrics :
사용자가 생체 인식을 사용하여 인증해야 함을 나타냅니다. Touch ID 또는 Face ID를 사용할 수 없거나 등록하지 않은 경우, policy evaluation이 실패합니다.
Touch ID 및 Face ID인증은 모두 5회 이상 실패하면 다시 사용할 수 없으므로 다시 사용할려면 장치 암호를 입력해야 합니다.

deviceOwnerAuthentication
Touch ID 또는 Face ID가 등록되어 있고, 사용 중지 되지 않은 경우, 사용자에게 먼저 터치하라는 메세지가 표시됩니다.
그렇지 않은 경우, 장치 암호를 입력하라는 메세지가 표시됩니다. 장치 암호가 활성화되어 있지 않으면, policy evaluation이 실패합니다.
패스코드 인증은 6회 실패 이후에 비활성화 되며, 지연은 점진적으로 증가합니다.

현재 진행중인 프로젝트에서는 생체인증에 실패한 경우, 디바이스 패스코드로 대체하지 못 하도록 설정해야 함으로,

 

deviceOwnerAuthenticationWithBiometrics 정책을 사용하여 진행하였습니다.

 

또한 해당 함수에서 리턴되는 에러의 경우

 

"생체인증 기능을 사용할 수 없는 경우" 즉 LAError 타입이 리턴됩니다.

 

해당 에러가 리턴되는 경우로는, 

 

.passcodeNotSet: 기기 패스코드가 설정되어 있지 않는 경우

.invalidContext: 현재 LAContext가 비활성화 되어 있는 경우

.biometryLockout: 생체인증 실패 횟수 초과로 인해 비활성화 되어 있는 경우

.biometryNotAvailable: 기기에서 생체 인증 기능을 사용할 수 없는 경우

.biometryNotEnrolled: 기기에 생체인증 기능이 활성화 되어 있지 않은 경우

.userCancel: 사용자가 취소한 경우

 

정도가 있으며 해당 에러가 발생 시 노출될 문구를 설정하여 사용자로 하여금 적절한 조치를 취할수 있도록 하였습니다.

 

3. 생체인증 정보 변경 시 처리

해당 작업은 생체인증 관련 작업 시 가장 까다로운 작업이었으며, 

 

보안상 꼭 짚고 넘어가야하는 부분입니다.

 

사용자 기기의 패스코드가 타인에게 노출되었다고 가정해보겠습니다.

 

만약 타인이 기기를 취득했고 해당 앱에서 생체인증 기능이 활성화되어 있다면,

 

기기에서 생체인증을 본인의 생체정보로 재설정한 이후,

 

앱에서 생체인증을 통해 접근할 수 있는 모든 기능을 사용할 수 있게 됩니다.

 

이러한 사고를 방지하기 위하여 기기의 생체 정보가 변경된 경우

 

앱에서 생체정보 변경을 감지 및 앱에서 생체 인증 기능을 재활성화하도록 유도합니다.

 

해당 기능을 구현하기 위해

 

최초 생체인증 기능을 활성화하는 경우 LAContext의 evenalutedPolicyDomainState를

 

Base64 인코딩된 문자열 형식으로 추출하여 기기의 키체인에 저장합니다.

 

추후 생체 인증을 진행하는 경우 같은 방법으로 문자열을 추출 및 기기 DB에 저장된 값과 비교합니다.

 

비교한 값이 같은 경우 정상적으로 인증 이후 의도한 기능에 접근시키며,

 

비교한 값이 다른 경우, 생체 정보에 변화가 있음으로 판단되어

 

즉시 생체 인증 기능을 비활성화 및 재활성화가 필요함을 사용자에게 알립니다.

 

class func requestBioAuth(context: LAContext = LAContext(),
                          type: BiometricType,
                          purpose: AuthPurpose? = nil) -> Observable<AuthResult> {
if 생체인증 활성화인 경우 {
    // skip detecting context change
    return Observable.evaluatePolicy(context: context)
        .flatMap { succeed -> Observable<AuthResult> in
            guard succeed else {
                // 생체인증 실패 -> 에러
            }
            guard let newContext 
                    = context.evaluatedPolicyDomainState?.base64EncodedString() else {
                // 생체정보 Context를 찾을수 없음 -> 에러
            }

            // 키체인에 생체인증 Context 정보 저장 (Observable)
        }
        .catch { error -> Observable<AuthResult> in
            return .just(.failure(ErrorHandler.convert(error)))
        }
} else {
    // detect context change
    // 키체인에 저장된 이전 생체 정보 호출
    return Observable.combineLatest(KeyChainManager.shared.getKeychainString(key: .BIO_CONTEXT),
                                    Observable.evaluatePolicy(context: context))
        .flatMap { oldContext, succeed -> Observable<AuthResult> in
            guard succeed else { 
                // 생체인증 실패 -> 에러
            }
            guard let newContext
                     = context.evaluatedPolicyDomainState?.base64EncodedString() else {
                // 생체정보 Context를 찾을수 없음 -> 에러
            }   
        
            // 생체정보 비교
            switch oldContext == newContext {
            case true:
                return .just(.success)
            case false:
                return .just(.failure(.KNOWN(title: "생체정보가 변경되었습니다.",
                                             reason: "생체인증 기능을 재활성화해주세요")))
            }
        
        }
        .catch { error -> Observable<AuthResult> in
            return .just(.failure(ErrorHandler.convert(error)))
        }
    }
}

 

해당 기능을 구현함으로서 

 

요구사항을 모두 충족시켰습니다.

 

해당 기능을 구현한 코드들 또한 추후에 별도 프로젝트로 구분하여 업로드하도록 하겠습니다.

 

해당 코드는 아래 깃에서 확인해주시면 감사하겠습니다!

 

https://github.com/rnjstjddn96/LocalAuthentification

 

 

긴 글 읽어주셔서 감사합니다!

'Swift' 카테고리의 다른 글

보안 핀코드 입력화면 제작기  (0) 2022.05.15
Initializer  (0) 2022.01.27
Memory에 대한 이해  (0) 2021.02.28
Lazy in Swift  (0) 2021.02.28