chromium/chrome/browser/ui/views/webauthn/mac_authentication_view.mm

// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/ui/views/webauthn/mac_authentication_view.h"

#import <LocalAuthentication/LocalAuthentication.h>
#import <LocalAuthenticationEmbeddedUI/LocalAuthenticationEmbeddedUI.h>

#include "base/logging.h"
#include "base/strings/sys_string_conversions.h"
#include "base/timer/timer.h"
#include "components/device_event_log/device_event_log.h"
#include "content/public/browser/browser_thread.h"
#include "crypto/scoped_lacontext.h"
#include "device/fido/mac/util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/gfx/canvas.h"
#include "ui/views/widget/widget.h"

// kWidth is the width (and height, since it's square) of the NSView.
constexpr int kWidth = 64;

// The seconds it takes for the Touch ID animation to finish when the challenge
// fails.
constexpr float kErrorAnimationLength = 1;

// The seconds it takes for the Touch ID animation to finish when the challenge
// succeeds. This is trimmed down so that we can overlap the enclave operation
// with the animation.
constexpr float kSuccessAnimationLength = 1.6;

struct API_AVAILABLE(macos(12.0)) MacAuthenticationView::ObjCStorage {
  LAContext* __strong context;
  LAAuthenticationView* __strong auth_view;
};

MacAuthenticationView::MacAuthenticationView(Callback callback,
                                             std::u16string touch_id_reason)
    : callback_(std::move(callback)),
      storage_(std::make_unique<ObjCStorage>()),
      touch_id_reason_(std::move(touch_id_reason)) {
  storage_->context = [[LAContext alloc] init];
  storage_->auth_view =
      [[LAAuthenticationView alloc] initWithContext:storage_->context];

  // The size of the NSView is set as constraints on itself. But none of the
  // standard sizes match the size used in Safari. Thus we erase them set our
  // own.
  [storage_->auth_view removeConstraints:storage_->auth_view.constraints];
  [storage_->auth_view.widthAnchor constraintEqualToConstant:kWidth].active =
      YES;
  [storage_->auth_view.heightAnchor constraintEqualToConstant:kWidth].active =
      YES;
  return;
}

MacAuthenticationView::~MacAuthenticationView() = default;

gfx::Size MacAuthenticationView::CalculatePreferredSize(
    const views::SizeBounds& available_size) const {
  return gfx::Size(kWidth, kWidth);
}

void MacAuthenticationView::AddedToWidget() {
  // An `LAAuthenticationView` is an NSView, but Chromium uses the Views
  // framework for its UI, which just renders pixels onto a backing surface
  // (called a "widget"). Thus any NSViews have to be composited onto what
  // Views renders and positioned in the right place.
  //
  // Here the `LAAuthenticationView` is added to the NSWindow for this View, and
  // positioned above it so that it's painted on top. We assume that the first
  // existing NSView in the NSWindow is the `ViewsCompositorSuperview` that
  // Views renders onto.
  NSWindow* window = GetWidget()->GetNativeWindow().GetNativeNSWindow();
  [window.contentView addSubview:storage_->auth_view
                      positioned:NSWindowAbove
                      relativeTo:window.contentView.subviews[0]];
}

void MacAuthenticationView::RemovedFromWidget() {
  [storage_->auth_view removeFromSuperview];
}

void MacAuthenticationView::Layout(PassKey) {
  gfx::Rect bounds = this->bounds();
  // The bounds of this View include the offset from its parent View.
  // However, `ConvertPointToWidget` (below) already takes this offset into
  // account so it must be zeroed here to avoid it being applied twice.
  bounds.set_x(0);
  bounds.set_y(0);
  // The LAAuthenticationView is centered within this View.
  bounds.ClampToCenteredSize(gfx::Size(kWidth, kWidth));
  // The Widget represents the backing surface onto which Views renders. We need
  // to know the position of the LAAuthenticationView in relation to the widget
  // because NSView doesn't know anything about Chromium's Views tree.
  gfx::Point point = bounds.origin();
  View::ConvertPointToWidget(this, &point);
  // Views puts (0, 0) at the top left and positive-y is downwards. But NSView
  // puts (0,0) at the bottom-left and positive-y is up. So we need to know the
  // height of the Widget to adjust things.
  gfx::Rect widget_rect = this->GetWidget()->GetClientAreaBoundsInScreen();
  // Place the LAAuthenticationView after adjusting for the different coordinate
  // system.
  storage_->auth_view.frame =
      NSMakeRect(point.x(), widget_rect.height() - point.y() - bounds.height(),
                 kWidth, kWidth);
}

void MacAuthenticationView::OnPaint(gfx::Canvas* canvas) {
  views::View::OnPaint(canvas);
  if (GetVisible() && !evaluation_requested_) {
    InvalidateLayout();
    storage_->auth_view.hidden = false;
    evaluation_requested_ = true;
    if (!device::fido::mac::DeviceHasBiometricsAvailable()) {
      return;
    }
    __block auto internal_callback =
        base::BindOnce(&MacAuthenticationView::OnAuthenticationComplete,
                       weak_factory_.GetWeakPtr());
    [storage_->context
         evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics
        localizedReason:base::SysUTF16ToNSString(touch_id_reason_)
                  reply:^(BOOL success, NSError* error) {
                    if (error) {
                      FIDO_LOG(ERROR) << "Touch ID failed with error: "
                                      << error.localizedDescription.UTF8String;
                    }
                    content::BrowserThread::GetTaskRunnerForThread(
                        content::BrowserThread::UI)
                        ->PostTask(FROM_HERE,
                                   base::BindOnce(std::move(internal_callback),
                                                  success));
                  }];
  }
}

void MacAuthenticationView::VisibilityChanged(views::View* from,
                                              bool is_visible) {
  views::View::VisibilityChanged(from, is_visible);
  storage_->auth_view.hidden = !is_visible;
}

void MacAuthenticationView::OnAuthenticationComplete(bool success) {
  DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
  // It takes a while for the Touch ID animation to finish after success is
  // reported. Avoid jank by waiting for the animation to finish before
  // notifying the client.
  touch_id_animation_timer_.Start(
      FROM_HERE,
      base::Seconds(success ? kSuccessAnimationLength : kErrorAnimationLength),
      base::BindOnce(&MacAuthenticationView::OnTouchIDAnimationComplete,
                     base::Unretained(this), success));
}

void MacAuthenticationView::OnTouchIDAnimationComplete(bool success) {
  std::optional<crypto::ScopedLAContext> lacontext;
  if (success) {
    lacontext.emplace(storage_->context);
  }
  storage_->context = nil;
  std::move(callback_).Run(std::move(lacontext));
}

BEGIN_METADATA(MacAuthenticationView)
END_METADATA