chromium/components/webauthn/android/webauthn_browser_bridge.cc

// Copyright 2022 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/webauthn/android/webauthn_browser_bridge.h"

#include <jni.h>

#include "base/android/callback_android.h"
#include "base/android/jni_array.h"
#include "base/android/jni_string.h"
#include "base/android/scoped_java_ref.h"
#include "components/webauthn/android/webauthn_client_android.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "device/fido/discoverable_credential_metadata.h"
#include "device/fido/public_key_credential_user_entity.h"

// Must come after all headers that specialize FromJniType() / ToJniType().
#include "components/webauthn/android/jni_headers/WebauthnBrowserBridge_jni.h"

using base::android::ConvertJavaStringToUTF8;
using base::android::ScopedJavaGlobalRef;
using base::android::ScopedJavaLocalRef;

namespace webauthn {

device::DiscoverableCredentialMetadata ConvertJavaCredentialDetailsToMetadata(
    JNIEnv* env,
    ScopedJavaLocalRef<jobject> j_credential) {
  device::DiscoverableCredentialMetadata credential;
  base::android::JavaByteArrayToByteVector(
      env,
      Java_WebauthnBrowserBridge_getWebauthnCredentialDetailsCredentialId(
          env, j_credential),
      &credential.cred_id);
  base::android::JavaByteArrayToByteVector(
      env,
      Java_WebauthnBrowserBridge_getWebauthnCredentialDetailsUserId(
          env, j_credential),
      &credential.user.id);
  credential.user.name = ConvertJavaStringToUTF8(
      env, Java_WebauthnBrowserBridge_getWebauthnCredentialDetailsUserName(
               env, j_credential));
  credential.user.display_name = ConvertJavaStringToUTF8(
      env,
      Java_WebauthnBrowserBridge_getWebauthnCredentialDetailsUserDisplayName(
          env, j_credential));
  return credential;
}

void ConvertJavaCredentialArrayToMetadataVector(
    JNIEnv* env,
    const base::android::JavaRef<jobjectArray>& array,
    std::vector<device::DiscoverableCredentialMetadata>* out) {
  jsize jlength = env->GetArrayLength(array.obj());
  // GetArrayLength() returns -1 if |array| is not a valid Java array.
  DCHECK_GE(jlength, 0) << "Invalid array length: " << jlength;
  size_t length = static_cast<size_t>(std::max(0, jlength));
  for (size_t i = 0; i < length; ++i) {
    ScopedJavaLocalRef<jobject> j_credential(
        env, static_cast<jobject>(env->GetObjectArrayElement(array.obj(), i)));
    out->emplace_back(
        ConvertJavaCredentialDetailsToMetadata(env, j_credential));
  }
}

void OnWebauthnCredentialSelected(
    const base::android::JavaRef<jobject>& jcallback,
    const std::vector<uint8_t>& credential_id) {
  base::android::RunObjectCallbackAndroid(
      jcallback, base::android::ToJavaByteArray(
                     base::android::AttachCurrentThread(), credential_id));
}

void OnHybridAssertionInvoked(
    const base::android::JavaRef<jobject>& jcallback) {
  base::android::RunRunnableAndroid(jcallback);
}

static jlong JNI_WebauthnBrowserBridge_CreateNativeWebauthnBrowserBridge(
    JNIEnv* env,
    const base::android::JavaParamRef<jobject>& jbridge) {
  return reinterpret_cast<jlong>(new WebauthnBrowserBridge(env, jbridge));
}

WebauthnBrowserBridge::WebauthnBrowserBridge(
    JNIEnv* env,
    const base::android::JavaParamRef<jobject>& jbridge)
    : owner_(env, jbridge) {}

WebauthnBrowserBridge::~WebauthnBrowserBridge() = default;

void WebauthnBrowserBridge::OnCredentialsDetailsListReceived(
    JNIEnv* env,
    const base::android::JavaParamRef<jobject>&,
    const base::android::JavaParamRef<jobjectArray>& credentials,
    const base::android::JavaParamRef<jobject>& jframe_host,
    jboolean is_conditional_request,
    const base::android::JavaParamRef<jobject>& jget_assertion_callback,
    const base::android::JavaParamRef<jobject>& jhybrid_callback) const {
  auto* client = WebAuthnClientAndroid::GetClient();
  auto* render_frame_host =
      content::RenderFrameHost::FromJavaRenderFrameHost(jframe_host);
  // A null client indicates the embedder does not support Conditional UI.
  // Also, crash reports suggest that there can be null WebContents at this
  // point, presumably indicating that a tab is being closed while the
  // listCredentials call is outstanding. See https://crbug.com/1399887.
  if (!client || !render_frame_host ||
      !content::WebContents::FromRenderFrameHost(render_frame_host)) {
    std::vector<uint8_t> credential_id = {};
    base::android::RunObjectCallbackAndroid(
        jget_assertion_callback,
        base::android::ToJavaByteArray(base::android::AttachCurrentThread(),
                                       credential_id));
    return;
  }

  std::vector<device::DiscoverableCredentialMetadata> credentials_metadata;
  ConvertJavaCredentialArrayToMetadataVector(env, credentials,
                                             &credentials_metadata);

  base::RepeatingCallback<void()> hybrid_callback;
  if (jhybrid_callback != nullptr) {
    hybrid_callback = base::BindRepeating(
        &OnHybridAssertionInvoked,
        ScopedJavaGlobalRef<jobject>(env, jhybrid_callback));
  }

  client->OnWebAuthnRequestPending(
      render_frame_host, credentials_metadata, is_conditional_request,
      base::BindRepeating(
          &OnWebauthnCredentialSelected,
          ScopedJavaGlobalRef<jobject>(env, jget_assertion_callback)),
      std::move(hybrid_callback));
}

void TriggerFullRequest(
    const base::android::JavaRef<jobject>& jfull_request_runnable,
    bool request_passwords) {
  base::android::RunBooleanCallbackAndroid(jfull_request_runnable,
                                           request_passwords);
}

void WebauthnBrowserBridge::OnCredManConditionalRequestPending(
    JNIEnv* env,
    const base::android::JavaParamRef<jobject>& jframe_host,
    jboolean jhas_results,
    const base::android::JavaParamRef<jobject>& jfull_request_runnable) {
  auto* client = WebAuthnClientAndroid::GetClient();
  auto* render_frame_host =
      content::RenderFrameHost::FromJavaRenderFrameHost(jframe_host);
  if (!client || !render_frame_host ||
      !content::WebContents::FromRenderFrameHost(render_frame_host)) {
    return;
  }
  client->OnCredManConditionalRequestPending(
      render_frame_host, jhas_results,
      base::BindRepeating(
          &TriggerFullRequest,
          ScopedJavaGlobalRef<jobject>(env, jfull_request_runnable)));
}

void WebauthnBrowserBridge::OnCredManUiClosed(
    JNIEnv* env,
    const base::android::JavaParamRef<jobject>& jframe_host,
    jboolean jsuccess) {
  auto* client = WebAuthnClientAndroid::GetClient();
  auto* render_frame_host =
      content::RenderFrameHost::FromJavaRenderFrameHost(jframe_host);
  if (!client || !render_frame_host ||
      !content::WebContents::FromRenderFrameHost(render_frame_host)) {
    return;
  }
  client->OnCredManUiClosed(render_frame_host, jsuccess);
}

void WebauthnBrowserBridge::OnPasswordCredentialReceived(
    JNIEnv* env,
    const base::android::JavaParamRef<jobject>& jframe_host,
    const base::android::JavaParamRef<jstring>& jusername,
    const base::android::JavaParamRef<jstring>& jpassword) {
  auto* client = WebAuthnClientAndroid::GetClient();
  auto* render_frame_host =
      content::RenderFrameHost::FromJavaRenderFrameHost(jframe_host);
  if (!client || !render_frame_host ||
      !content::WebContents::FromRenderFrameHost(render_frame_host)) {
    return;
  }
  client->OnPasswordCredentialReceived(
      render_frame_host,
      base::android::ConvertJavaStringToUTF16(env, jusername),
      base::android::ConvertJavaStringToUTF16(env, jpassword));
}

void WebauthnBrowserBridge::CleanupRequest(
    JNIEnv* env,
    const base::android::JavaParamRef<jobject>& jframe_host) const {
  auto* client = WebAuthnClientAndroid::GetClient();
  auto* render_frame_host =
      content::RenderFrameHost::FromJavaRenderFrameHost(jframe_host);

  // Crash reports indicate that there can be null WebContents at this point,
  // although it isn't clear how, since the Cancel message was received from
  // renderer and is processed synchronously. The null check exists to mitigate
  // downstream dereferences. See https://crbug.com/1399887.
  if (!render_frame_host ||
      !content::WebContents::FromRenderFrameHost(render_frame_host)) {
    return;
  }

  client->CleanupWebAuthnRequest(render_frame_host);
}

void WebauthnBrowserBridge::CleanupCredManRequest(
    JNIEnv* env,
    const base::android::JavaParamRef<jobject>& jframe_host) const {
  auto* client = WebAuthnClientAndroid::GetClient();
  auto* render_frame_host =
      content::RenderFrameHost::FromJavaRenderFrameHost(jframe_host);

  if (!client || !render_frame_host ||
      !content::WebContents::FromRenderFrameHost(render_frame_host)) {
    return;
  }
  client->CleanupCredManRequest(render_frame_host);
}

void WebauthnBrowserBridge::Destroy(JNIEnv* env) {
  delete this;
}

}  // namespace webauthn