| // Copyright 2020 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "device/fido/auth_token_requester.h" |
| |
| #include <set> |
| #include <utility> |
| |
| #include "base/bind.h" |
| #include "base/containers/contains.h" |
| #include "base/logging.h" |
| #include "base/stl_util.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "components/device_event_log/device_event_log.h" |
| #include "device/fido/authenticator_supported_options.h" |
| #include "device/fido/fido_authenticator.h" |
| #include "device/fido/fido_constants.h" |
| |
| namespace device { |
| |
| using ClientPinAvailability = |
| AuthenticatorSupportedOptions::ClientPinAvailability; |
| using UserVerificationAvailability = |
| AuthenticatorSupportedOptions::UserVerificationAvailability; |
| using BioEnrollmentAvailability = |
| AuthenticatorSupportedOptions::BioEnrollmentAvailability; |
| |
| AuthTokenRequester::Delegate::~Delegate() = default; |
| |
| AuthTokenRequester::Options::Options() = default; |
| AuthTokenRequester::Options::Options(Options&&) = default; |
| AuthTokenRequester::Options& AuthTokenRequester::Options::operator=(Options&&) = |
| default; |
| AuthTokenRequester::Options::~Options() = default; |
| |
| AuthTokenRequester::AuthTokenRequester(Delegate* delegate, |
| FidoAuthenticator* authenticator, |
| Options options) |
| : delegate_(delegate), |
| authenticator_(authenticator), |
| options_(std::move(options)), |
| internal_uv_locked_(options_.internal_uv_locked) { |
| DCHECK(delegate_); |
| DCHECK(authenticator_); |
| DCHECK(authenticator_->Options()); |
| DCHECK(!options_.token_permissions.empty()); |
| DCHECK(!options_.rp_id || !options_.rp_id->empty()); |
| // Authenticators with CTAP2.0-style pinToken support only support certain |
| // default permissions. |
| DCHECK( |
| authenticator_->Options()->supports_pin_uv_auth_token || |
| base::STLSetDifference<std::set<pin::Permissions>>( |
| options_.token_permissions, |
| std::set<pin::Permissions>{pin::Permissions::kMakeCredential, |
| pin::Permissions::kGetAssertion, |
| pin::Permissions::kBioEnrollment, |
| pin::Permissions::kCredentialManagement}) |
| .empty()); |
| } |
| |
| AuthTokenRequester::~AuthTokenRequester() = default; |
| |
| void AuthTokenRequester::ObtainPINUVAuthToken() { |
| if (authenticator_->Options()->supports_pin_uv_auth_token) { |
| // Only attempt to obtain a token through internal UV if the authenticator |
| // supports CTAP 2.1 pinUvAuthTokens. If it does not, it could be a 2.0 |
| // authenticator that supports UV without any sort of token. |
| const UserVerificationAvailability user_verification_availability = |
| authenticator_->Options()->user_verification_availability; |
| switch (user_verification_availability) { |
| case UserVerificationAvailability::kNotSupported: |
| case UserVerificationAvailability::kSupportedButNotConfigured: |
| // Try PIN first. |
| break; |
| case UserVerificationAvailability::kSupportedAndConfigured: |
| ObtainTokenFromInternalUV(); |
| return; |
| } |
| } |
| |
| const ClientPinAvailability client_pin_availability = |
| authenticator_->Options()->client_pin_availability; |
| switch (client_pin_availability) { |
| case ClientPinAvailability::kNotSupported: |
| delegate_->HavePINUVAuthTokenResultForAuthenticator( |
| authenticator_, Result::kPreTouchUnsatisfiableRequest, absl::nullopt); |
| return; |
| case ClientPinAvailability::kSupportedAndPinSet: |
| if (options_.skip_pin_touch) { |
| ObtainTokenFromPIN(); |
| return; |
| } |
| authenticator_->GetTouch(base::BindOnce( |
| &AuthTokenRequester::ObtainTokenFromPIN, weak_factory_.GetWeakPtr())); |
| return; |
| case ClientPinAvailability::kSupportedButPinNotSet: |
| if (options_.skip_pin_touch) { |
| ObtainTokenFromNewPIN(); |
| return; |
| } |
| authenticator_->GetTouch( |
| base::BindOnce(&AuthTokenRequester::ObtainTokenFromNewPIN, |
| weak_factory_.GetWeakPtr())); |
| return; |
| } |
| } |
| |
| void AuthTokenRequester::ObtainTokenFromInternalUV() { |
| authenticator_->GetUvRetries(base::BindOnce( |
| &AuthTokenRequester::OnGetUVRetries, weak_factory_.GetWeakPtr())); |
| } |
| |
| void AuthTokenRequester::OnGetUVRetries( |
| CtapDeviceResponseCode status, |
| absl::optional<pin::RetriesResponse> response) { |
| if (status != CtapDeviceResponseCode::kSuccess) { |
| delegate_->HavePINUVAuthTokenResultForAuthenticator( |
| authenticator_, Result::kPreTouchAuthenticatorResponseInvalid, |
| absl::nullopt); |
| return; |
| } |
| |
| internal_uv_locked_ = response->retries == 0; |
| if (response->retries == 0) { |
| // The authenticator was locked prior to calling |
| // ObtainTokenFromInternalUV(). Fall back to PIN if able. |
| if (authenticator_->Options()->client_pin_availability == |
| ClientPinAvailability::kSupportedAndPinSet) { |
| if (options_.skip_pin_touch) { |
| ObtainTokenFromPIN(); |
| return; |
| } |
| authenticator_->GetTouch(base::BindOnce( |
| &AuthTokenRequester::ObtainTokenFromPIN, weak_factory_.GetWeakPtr())); |
| return; |
| } |
| authenticator_->GetTouch(base::BindOnce( |
| &AuthTokenRequester::NotifyAuthenticatorSelectedAndFailWithResult, |
| weak_factory_.GetWeakPtr(), |
| Result::kPostTouchAuthenticatorInternalUVLock)); |
| return; |
| } |
| |
| if (is_internal_uv_retry_) { |
| delegate_->PromptForInternalUVRetry(response->retries); |
| } |
| authenticator_->GetUvToken({std::begin(options_.token_permissions), |
| std::end(options_.token_permissions)}, |
| options_.rp_id, |
| base::BindOnce(&AuthTokenRequester::OnGetUVToken, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| void AuthTokenRequester::OnGetUVToken( |
| CtapDeviceResponseCode status, |
| absl::optional<pin::TokenResponse> response) { |
| if (!base::Contains( |
| std::set<CtapDeviceResponseCode>{ |
| CtapDeviceResponseCode::kCtap2ErrUvInvalid, |
| CtapDeviceResponseCode::kCtap2ErrOperationDenied, |
| CtapDeviceResponseCode::kCtap2ErrUvBlocked, |
| CtapDeviceResponseCode::kSuccess}, |
| status)) { |
| // The request was rejected outright, no touch occurred. |
| FIDO_LOG(ERROR) << "Ignoring status " << static_cast<int>(status) |
| << " from " << authenticator_->GetDisplayName(); |
| delegate_->HavePINUVAuthTokenResultForAuthenticator( |
| authenticator_, Result::kPreTouchAuthenticatorResponseInvalid, |
| absl::nullopt); |
| return; |
| } |
| |
| if (!NotifyAuthenticatorSelected()) { |
| return; |
| } |
| |
| if (status == CtapDeviceResponseCode::kCtap2ErrOperationDenied) { |
| // The user explicitly denied to the operation on an authenticator with |
| // a display. |
| delegate_->HavePINUVAuthTokenResultForAuthenticator( |
| authenticator_, Result::kPostTouchAuthenticatorOperationDenied, |
| absl::nullopt); |
| return; |
| } |
| |
| if (status == CtapDeviceResponseCode::kCtap2ErrUvInvalid) { |
| // The attempt failed, but a retry is possible. |
| is_internal_uv_retry_ = true; |
| ObtainTokenFromInternalUV(); |
| return; |
| } |
| |
| if (status == CtapDeviceResponseCode::kCtap2ErrUvBlocked) { |
| // Fall back to PIN if able. |
| if (authenticator_->Options()->client_pin_availability == |
| ClientPinAvailability::kSupportedAndPinSet) { |
| internal_uv_locked_ = true; |
| ObtainTokenFromPIN(); |
| return; |
| } |
| // This can be returned pre-touch if the authenticator was already locked at |
| // the time GetUvToken() was called. However, we checked the number of |
| // remaining retries just before that to handle that case. |
| delegate_->HavePINUVAuthTokenResultForAuthenticator( |
| authenticator_, Result::kPostTouchAuthenticatorInternalUVLock, |
| absl::nullopt); |
| return; |
| } |
| |
| DCHECK_EQ(status, CtapDeviceResponseCode::kSuccess); |
| |
| delegate_->HavePINUVAuthTokenResultForAuthenticator( |
| authenticator_, Result::kSuccess, *response); |
| } |
| |
| void AuthTokenRequester::ObtainTokenFromPIN() { |
| if (NotifyAuthenticatorSelected()) { |
| authenticator_->GetPinRetries(base::BindOnce( |
| &AuthTokenRequester::OnGetPINRetries, weak_factory_.GetWeakPtr())); |
| } |
| } |
| |
| void AuthTokenRequester::OnGetPINRetries( |
| CtapDeviceResponseCode status, |
| absl::optional<pin::RetriesResponse> response) { |
| if (status != CtapDeviceResponseCode::kSuccess) { |
| delegate_->HavePINUVAuthTokenResultForAuthenticator( |
| authenticator_, Result::kPostTouchAuthenticatorResponseInvalid, |
| absl::nullopt); |
| return; |
| } |
| if (response->retries == 0) { |
| delegate_->HavePINUVAuthTokenResultForAuthenticator( |
| authenticator_, Result::kPostTouchAuthenticatorPINHardLock, |
| absl::nullopt); |
| return; |
| } |
| pin_retries_ = response->retries; |
| pin::PINEntryError error; |
| if (pin_invalid_) { |
| pin_invalid_ = false; |
| error = pin::PINEntryError::kWrongPIN; |
| } else if (internal_uv_locked_) { |
| error = pin::PINEntryError::kInternalUvLocked; |
| } else { |
| error = pin::PINEntryError::kNoError; |
| } |
| delegate_->CollectPIN( |
| pin::PINEntryReason::kChallenge, error, |
| authenticator_->CurrentMinPINLength(), pin_retries_, |
| base::BindOnce(&AuthTokenRequester::HavePIN, weak_factory_.GetWeakPtr())); |
| } |
| |
| void AuthTokenRequester::HavePIN(std::u16string pin16) { |
| pin::PINEntryError error = pin::ValidatePIN( |
| pin16, authenticator_->CurrentMinPINLength(), current_pin_); |
| if (error != pin::PINEntryError::kNoError) { |
| delegate_->CollectPIN(pin::PINEntryReason::kChallenge, error, |
| authenticator_->CurrentMinPINLength(), pin_retries_, |
| base::BindOnce(&AuthTokenRequester::HavePIN, |
| weak_factory_.GetWeakPtr())); |
| return; |
| } |
| |
| std::string pin = base::UTF16ToUTF8(pin16); |
| authenticator_->GetPINToken(pin, |
| {std::begin(options_.token_permissions), |
| std::end(options_.token_permissions)}, |
| options_.rp_id, |
| base::BindOnce(&AuthTokenRequester::OnGetPINToken, |
| weak_factory_.GetWeakPtr(), pin)); |
| return; |
| } |
| |
| void AuthTokenRequester::OnGetPINToken( |
| std::string pin, |
| CtapDeviceResponseCode status, |
| absl::optional<pin::TokenResponse> response) { |
| if (status == CtapDeviceResponseCode::kCtap2ErrPinInvalid) { |
| pin_invalid_ = true; |
| ObtainTokenFromPIN(); |
| return; |
| } |
| |
| if (status != CtapDeviceResponseCode::kSuccess) { |
| Result ret; |
| switch (status) { |
| case CtapDeviceResponseCode::kCtap2ErrPinPolicyViolation: |
| // The user needs to set a new PIN before they can use the device. |
| current_pin_ = pin; |
| delegate_->CollectPIN(pin::PINEntryReason::kChange, |
| pin::PINEntryError::kNoError, |
| authenticator_->NewMinPINLength(), |
| /*attempts=*/0, |
| base::BindOnce(&AuthTokenRequester::HaveNewPIN, |
| weak_factory_.GetWeakPtr())); |
| return; |
| case CtapDeviceResponseCode::kCtap2ErrPinAuthBlocked: |
| ret = Result::kPostTouchAuthenticatorPINSoftLock; |
| break; |
| case CtapDeviceResponseCode::kCtap2ErrPinBlocked: |
| ret = Result::kPostTouchAuthenticatorPINHardLock; |
| break; |
| default: |
| ret = Result::kPostTouchAuthenticatorResponseInvalid; |
| break; |
| } |
| delegate_->HavePINUVAuthTokenResultForAuthenticator(authenticator_, ret, |
| absl::nullopt); |
| return; |
| } |
| |
| delegate_->HavePINUVAuthTokenResultForAuthenticator( |
| authenticator_, Result::kSuccess, std::move(*response)); |
| } |
| |
| void AuthTokenRequester::ObtainTokenFromNewPIN() { |
| if (NotifyAuthenticatorSelected()) { |
| delegate_->CollectPIN(pin::PINEntryReason::kSet, |
| pin::PINEntryError::kNoError, |
| authenticator_->NewMinPINLength(), |
| /*attempts=*/0, |
| base::BindOnce(&AuthTokenRequester::HaveNewPIN, |
| weak_factory_.GetWeakPtr())); |
| } |
| } |
| |
| void AuthTokenRequester::HaveNewPIN(std::u16string pin16) { |
| pin::PINEntryError error = |
| pin::ValidatePIN(pin16, authenticator_->NewMinPINLength(), current_pin_); |
| if (error != pin::PINEntryError::kNoError) { |
| delegate_->CollectPIN( |
| current_pin_ ? pin::PINEntryReason::kChange : pin::PINEntryReason::kSet, |
| error, authenticator_->NewMinPINLength(), |
| /*attempts=*/0, |
| base::BindOnce(&AuthTokenRequester::HaveNewPIN, |
| weak_factory_.GetWeakPtr())); |
| return; |
| } |
| |
| std::string pin = base::UTF16ToUTF8(pin16); |
| if (current_pin_) { |
| authenticator_->ChangePIN(*current_pin_, pin, |
| base::BindOnce(&AuthTokenRequester::OnSetPIN, |
| weak_factory_.GetWeakPtr(), pin)); |
| return; |
| } |
| authenticator_->SetPIN(pin, base::BindOnce(&AuthTokenRequester::OnSetPIN, |
| weak_factory_.GetWeakPtr(), pin)); |
| return; |
| } |
| |
| void AuthTokenRequester::OnSetPIN(std::string pin, |
| CtapDeviceResponseCode status, |
| absl::optional<pin::EmptyResponse> response) { |
| if (status != CtapDeviceResponseCode::kSuccess) { |
| delegate_->HavePINUVAuthTokenResultForAuthenticator( |
| authenticator_, Result::kPostTouchAuthenticatorResponseInvalid, |
| absl::nullopt); |
| return; |
| } |
| |
| // Having just set the PIN, we need to immediately turn around and use it to |
| // get a PIN token. |
| authenticator_->GetPINToken(std::move(pin), |
| {std::begin(options_.token_permissions), |
| std::end(options_.token_permissions)}, |
| options_.rp_id, |
| base::BindOnce(&AuthTokenRequester::OnGetPINToken, |
| weak_factory_.GetWeakPtr(), pin)); |
| } |
| |
| bool AuthTokenRequester::NotifyAuthenticatorSelected() { |
| if (!authenticator_selected_result_.has_value()) { |
| authenticator_selected_result_ = |
| delegate_->AuthenticatorSelectedForPINUVAuthToken(authenticator_); |
| } |
| return *authenticator_selected_result_; |
| } |
| |
| void AuthTokenRequester::NotifyAuthenticatorSelectedAndFailWithResult( |
| Result result) { |
| if (NotifyAuthenticatorSelected()) { |
| delegate_->HavePINUVAuthTokenResultForAuthenticator(authenticator_, result, |
| absl::nullopt); |
| } |
| } |
| |
| } // namespace device |