blob: 3aee9cd468e77b1bf931109a9f1f90aae4cb402b [file] [log] [blame]
// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/payments/content/secure_payment_confirmation_app.h"
#include <cstdint>
#include <optional>
#include <utility>
#include <vector>
#include "base/base64.h"
#include "base/base64url.h"
#include "base/check.h"
#include "base/containers/flat_tree.h"
#include "base/containers/to_vector.h"
#include "base/feature_list.h"
#include "base/json/json_writer.h"
#include "base/metrics/histogram_functions.h"
#include "base/notreached.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "base/values.h"
#include "components/payments/content/browser_binding/browser_bound_key.h"
#include "components/payments/content/browser_binding/passkey_browser_binder.h"
#include "components/payments/content/payment_request_spec.h"
#include "components/payments/core/error_strings.h"
#include "components/payments/core/features.h"
#include "components/payments/core/method_strings.h"
#include "components/payments/core/payer_data.h"
#include "components/payments/core/payments_experimental_features.h"
#include "components/payments/core/secure_payment_confirmation_metrics.h"
#include "components/webauthn/core/browser/internal_authenticator.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_features.h"
#include "crypto/sha2.h"
#include "device/fido/fido_transport_protocol.h"
#include "device/fido/fido_types.h"
#include "device/fido/public_key_credential_descriptor.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/mojom/webauthn/authenticator.mojom.h"
#include "url/url_constants.h"
namespace payments {
namespace {
static constexpr int kDefaultTimeoutMinutes = 3;
// Records UMA metric for the system prompt result.
void RecordSystemPromptResult(
const SecurePaymentConfirmationSystemPromptResult result) {
base::UmaHistogramEnumeration(
"PaymentRequest.SecurePaymentConfirmation.Funnel.SystemPromptResult",
result);
}
#if BUILDFLAG(IS_ANDROID)
const device::PublicKeyCredentialParams::CredentialInfo
kDefaultBrowserBoundKeyCredentialParameters[] = {
{device::CredentialType::kPublicKey,
base::strict_cast<int32_t>(device::CoseAlgorithmIdentifier::kEs256)},
{device::CredentialType::kPublicKey,
base::strict_cast<int32_t>(device::CoseAlgorithmIdentifier::kRs256)}};
#endif // BUILDFLAG(IS_ANDROID)
} // namespace
SecurePaymentConfirmationApp::SecurePaymentConfirmationApp(
content::WebContents* web_contents_to_observe,
const std::string& effective_relying_party_identity,
const std::u16string& payment_instrument_label,
const std::u16string& payment_instrument_details,
std::unique_ptr<SkBitmap> payment_instrument_icon,
std::vector<uint8_t> credential_id,
std::unique_ptr<PasskeyBrowserBinder> passkey_browser_binder,
bool device_supports_browser_bound_keys_in_hardware,
const url::Origin& merchant_origin,
base::WeakPtr<PaymentRequestSpec> spec,
mojom::SecurePaymentConfirmationRequestPtr request,
std::unique_ptr<webauthn::InternalAuthenticator> authenticator,
std::vector<PaymentApp::PaymentEntityLogo> payment_entities_logos)
: PaymentApp(/*icon_resource_id=*/0, PaymentApp::Type::INTERNAL),
content::WebContentsObserver(web_contents_to_observe),
authenticator_frame_routing_id_(
authenticator ? authenticator->GetRenderFrameHost()->GetGlobalId()
: content::GlobalRenderFrameHostId()),
effective_relying_party_identity_(effective_relying_party_identity),
payment_instrument_label_(payment_instrument_label),
payment_instrument_details_(payment_instrument_details),
payment_instrument_icon_(std::move(payment_instrument_icon)),
credential_id_(std::move(credential_id)),
merchant_origin_(merchant_origin),
spec_(spec),
request_(std::move(request)),
authenticator_(std::move(authenticator)),
passkey_browser_binder_(std::move(passkey_browser_binder)),
device_supports_browser_bound_keys_in_hardware_(
device_supports_browser_bound_keys_in_hardware),
payment_entities_logos_(std::move(payment_entities_logos)) {
app_method_names_.insert(methods::kSecurePaymentConfirmation);
}
SecurePaymentConfirmationApp::~SecurePaymentConfirmationApp() = default;
void SecurePaymentConfirmationApp::InvokePaymentApp(
base::WeakPtr<Delegate> delegate) {
if (!authenticator_ || !spec_)
return;
DCHECK(spec_->IsInitialized());
auto options = blink::mojom::PublicKeyCredentialRequestOptions::New();
options->relying_party_id = effective_relying_party_identity_;
options->timeout = request_->timeout.has_value()
? request_->timeout.value()
: base::Minutes(kDefaultTimeoutMinutes);
options->user_verification = device::UserVerificationRequirement::kRequired;
std::vector<device::PublicKeyCredentialDescriptor> credentials;
options->extensions =
!request_->extensions
? blink::mojom::AuthenticationExtensionsClientInputs::New()
: request_->extensions.Clone();
if (base::FeatureList::IsEnabled(
::features::kSecurePaymentConfirmationDebug)) {
options->user_verification =
device::UserVerificationRequirement::kPreferred;
// The `device::PublicKeyCredentialDescriptor` constructor with 2 parameters
// enables authentication through all protocols.
credentials.emplace_back(device::CredentialType::kPublicKey,
credential_id_);
} else {
// Enable authentication only through internal authenticators by default.
credentials.emplace_back(device::CredentialType::kPublicKey, credential_id_,
base::flat_set<device::FidoTransportProtocol>{
device::FidoTransportProtocol::kInternal});
}
options->allow_credentials = std::move(credentials);
options->challenge = request_->challenge;
std::optional<std::vector<uint8_t>> browser_bound_public_key = std::nullopt;
#if BUILDFLAG(IS_ANDROID)
if (passkey_browser_binder_) {
std::vector<device::PublicKeyCredentialParams::CredentialInfo>
credential_parameters = request_->browser_bound_pub_key_cred_params;
if (credential_parameters.empty()) {
credential_parameters =
base::ToVector(kDefaultBrowserBoundKeyCredentialParameters);
}
if (web_contents()->GetBrowserContext()->IsOffTheRecord()) {
passkey_browser_binder_->GetBoundKeyForPasskey(
credential_id_, effective_relying_party_identity_,
base::BindOnce(&SecurePaymentConfirmationApp::OnGetBrowserBoundKey,
weak_ptr_factory_.GetWeakPtr(), delegate,
std::move(options), /*is_new=*/false));
} else {
passkey_browser_binder_->GetOrCreateBoundKeyForPasskey(
credential_id_, effective_relying_party_identity_,
credential_parameters,
base::BindOnce(&SecurePaymentConfirmationApp::OnGetBrowserBoundKey,
weak_ptr_factory_.GetWeakPtr(), delegate,
std::move(options)));
}
} else {
OnGetBrowserBoundKey(delegate, std::move(options),
/*is_new=*/false, /*browser_bound_key=*/nullptr);
}
#else // BUILDFLAG(IS_ANDROID))
OnGetBrowserBoundKey(delegate, std::move(options),
/*is_new=*/false, /*browser_bound_key=*/nullptr);
#endif // BUILDFLAG(IS_ANDROID))
}
bool SecurePaymentConfirmationApp::IsCompleteForPayment() const {
return true;
}
bool SecurePaymentConfirmationApp::CanPreselect() const {
return true;
}
std::u16string SecurePaymentConfirmationApp::GetMissingInfoLabel() const {
NOTREACHED();
}
bool SecurePaymentConfirmationApp::HasEnrolledInstrument() const {
// If the fallback feature is disabled, the factory should only create this
// app if the authenticator and credentials were available. Therefore, this
// function can always return true with the fallback feature disabled.
return (authenticator_ && !credential_id_.empty()) ||
!(PaymentsExperimentalFeatures::IsEnabled(
features::kSecurePaymentConfirmationFallback) ||
base::FeatureList::IsEnabled(
blink::features::kSecurePaymentConfirmationUxRefresh));
}
bool SecurePaymentConfirmationApp::NeedsInstallation() const {
return false;
}
std::string SecurePaymentConfirmationApp::GetId() const {
if (credential_id_.empty()) {
CHECK(PaymentsExperimentalFeatures::IsEnabled(
features::kSecurePaymentConfirmationFallback) ||
base::FeatureList::IsEnabled(
blink::features::kSecurePaymentConfirmationUxRefresh));
// Since there is no credential_id_ in the fallback flow, we still must
// return a non-empty app ID.
return "spc";
} else {
return base::Base64Encode(credential_id_);
}
}
std::u16string SecurePaymentConfirmationApp::GetLabel() const {
return payment_instrument_label_;
}
std::u16string SecurePaymentConfirmationApp::GetSublabel() const {
return payment_instrument_details_;
}
const SkBitmap* SecurePaymentConfirmationApp::icon_bitmap() const {
return payment_instrument_icon_.get();
}
std::vector<PaymentApp::PaymentEntityLogo*>
SecurePaymentConfirmationApp::GetPaymentEntitiesLogos() {
// Filters logos with empty icons out from payment_entities_logos_. Once
// network_bitmap() and issuer_bitmap() are no longer needed,
// payment_entities_logos_ will no longer contain logos with empty icons, and
// the filtering will not be required.
//
// TODO(crbug.com/428009834): Validate this before removing the filtering. If
// a PaymentEntityLogo failed to download, the logo.icon would also be
// nullptr and so filtering may still be necessary.
std::vector<PaymentApp::PaymentEntityLogo*> filtered_logos;
for (PaymentApp::PaymentEntityLogo& logo : payment_entities_logos_) {
if (logo.icon != nullptr) {
filtered_logos.push_back(&logo);
}
}
return filtered_logos;
}
bool SecurePaymentConfirmationApp::IsValidForModifier(
const std::string& method) const {
bool is_valid = false;
IsValidForPaymentMethodIdentifier(method, &is_valid);
return is_valid;
}
base::WeakPtr<PaymentApp> SecurePaymentConfirmationApp::AsWeakPtr() {
return weak_ptr_factory_.GetWeakPtr();
}
bool SecurePaymentConfirmationApp::HandlesShippingAddress() const {
return false;
}
bool SecurePaymentConfirmationApp::HandlesPayerName() const {
return false;
}
bool SecurePaymentConfirmationApp::HandlesPayerEmail() const {
return false;
}
bool SecurePaymentConfirmationApp::HandlesPayerPhone() const {
return false;
}
bool SecurePaymentConfirmationApp::IsWaitingForPaymentDetailsUpdate() const {
return false;
}
void SecurePaymentConfirmationApp::UpdateWith(
mojom::PaymentRequestDetailsUpdatePtr details_update) {
NOTREACHED();
}
void SecurePaymentConfirmationApp::OnPaymentDetailsNotUpdated() {
NOTREACHED();
}
void SecurePaymentConfirmationApp::AbortPaymentApp(
base::OnceCallback<void(bool)> abort_callback) {
std::move(abort_callback).Run(/*abort_success=*/false);
}
mojom::PaymentResponsePtr
SecurePaymentConfirmationApp::SetAppSpecificResponseFields(
mojom::PaymentResponsePtr response) const {
blink::mojom::GetAssertionAuthenticatorResponsePtr assertion_response =
blink::mojom::GetAssertionAuthenticatorResponse::New(
response_->info.Clone(), response_->authenticator_attachment,
response_->signature, response_->user_handle,
response_->extensions.Clone());
#if BUILDFLAG(IS_ANDROID)
if (base::FeatureList::IsEnabled(
blink::features::kSecurePaymentConfirmationBrowserBoundKeys) &&
assertion_response->extensions->payment.is_null()) {
assertion_response->extensions->payment =
blink::mojom::AuthenticationExtensionsPaymentResponse::New();
}
if (browser_bound_key_) {
assertion_response->extensions->payment->browser_bound_signature =
browser_bound_key_->Sign(response_->info->client_data_json);
}
#endif // BUILDFLAG(IS_ANDROID)
response->get_assertion_authenticator_response =
std::move(assertion_response);
return response;
}
void SecurePaymentConfirmationApp::RenderFrameDeleted(
content::RenderFrameHost* render_frame_host) {
if (content::RenderFrameHost::FromID(authenticator_frame_routing_id_) ==
render_frame_host) {
// The authenticator requires to be deleted before the render frame.
authenticator_.reset();
}
}
PasskeyBrowserBinder*
SecurePaymentConfirmationApp::GetPasskeyBrowserBinderForTesting() {
return passkey_browser_binder_.get();
}
void SecurePaymentConfirmationApp::OnGetBrowserBoundKey(
base::WeakPtr<Delegate> delegate,
blink::mojom::PublicKeyCredentialRequestOptionsPtr options,
bool is_new,
std::unique_ptr<BrowserBoundKey> browser_bound_key) {
browser_bound_key_ = std::move(browser_bound_key);
std::optional<std::vector<uint8_t>> browser_bound_public_key = std::nullopt;
if (browser_bound_key_) {
browser_bound_public_key = browser_bound_key_->GetPublicKeyAsCoseKey();
RecordBrowserBoundKeyInclusion(
is_new ? SecurePaymentConfirmationBrowserBoundKeyInclusionResult::
kIncludedNew
: SecurePaymentConfirmationBrowserBoundKeyInclusionResult::
kIncludedExisting);
} else if (passkey_browser_binder_) {
RecordBrowserBoundKeyInclusion(
device_supports_browser_bound_keys_in_hardware_
? SecurePaymentConfirmationBrowserBoundKeyInclusionResult::
kNotIncludedWithDeviceHardware
: SecurePaymentConfirmationBrowserBoundKeyInclusionResult::
kNotIncludedWithoutDeviceHardware);
}
// TODO(crbug.com/40225659): The 'showOptOut' flag status must also be signed
// in the assertion, so that the verifier can check that the caller offered
// the experience if desired.
std::optional<std::vector<blink::mojom::ShownPaymentEntityLogoPtr>>
payment_entities_logos;
blink::mojom::PaymentCredentialInstrumentPtr instrument =
request_->instrument.Clone();
if (base::FeatureList::IsEnabled(
blink::features::kSecurePaymentConfirmationUxRefresh)) {
payment_entities_logos.emplace();
for (const PaymentApp::PaymentEntityLogo& logo : payment_entities_logos_) {
// When the logo could not be download or decoded, then logo.icon is null.
// In this case a ShownPaymentEntityLogo with an empty url is added, so
// that clientData includes a placeholder when images failed to download.
payment_entities_logos->push_back(
blink::mojom::ShownPaymentEntityLogo::New(
logo.icon ? logo.url : GURL::EmptyGURL(),
base::UTF16ToUTF8(logo.label)));
}
} else {
// If kSecurePaymentConfirmationUxRefresh is not enabled, then we did not
// show the instrument details in the UI, and therefore we do not include
// them in the clientData by setting to std::nullopt. Details should be
// std::nullopt here because the dictionary field is already flag protected
// on the render side; however, we also set it empty here on the
// browser-side as well.
instrument->details = std::nullopt;
}
authenticator_->SetPaymentOptions(blink::mojom::PaymentOptions::New(
spec_->GetTotal(/*selected_app=*/this)->amount.Clone(),
std::move(instrument), request_->payee_name, request_->payee_origin,
/*payment_entities_logos=*/std::move(payment_entities_logos),
std::move(browser_bound_public_key)));
authenticator_->GetAssertion(
std::move(options),
base::BindOnce(&SecurePaymentConfirmationApp::OnGetAssertion,
weak_ptr_factory_.GetWeakPtr(), delegate));
}
void SecurePaymentConfirmationApp::OnGetAssertion(
base::WeakPtr<Delegate> delegate,
blink::mojom::AuthenticatorStatus status,
blink::mojom::GetAssertionAuthenticatorResponsePtr response,
blink::mojom::WebAuthnDOMExceptionDetailsPtr dom_exception_details) {
if (!delegate)
return;
if (status != blink::mojom::AuthenticatorStatus::SUCCESS || !response) {
delegate->OnInstrumentDetailsError(
errors::kWebAuthnOperationTimedOutOrNotAllowed);
RecordSystemPromptResult(
SecurePaymentConfirmationSystemPromptResult::kCanceled);
return;
}
RecordSystemPromptResult(
SecurePaymentConfirmationSystemPromptResult::kAccepted);
response_ = std::move(response);
delegate->OnInstrumentDetailsReady(methods::kSecurePaymentConfirmation, "{}",
PayerData());
}
} // namespace payments