| // 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 <algorithm> |
| #include <cstddef> |
| #include <cstdint> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #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 "base/check_op.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback.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::AttachCurrentThread; |
| using base::android::ConvertJavaStringToUTF8; |
| using base::android::ConvertUTF16ToJavaString; |
| 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) { |
| auto j_credential = ScopedJavaLocalRef<jobject>::Adopt( |
| 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) { |
| JNIEnv* env = AttachCurrentThread(); |
| base::android::RunObjectCallbackAndroid( |
| jcallback, Java_WebauthnBrowserBridge_createSelectedPasskeyCredential( |
| env, base::android::ToJavaByteArray(env, credential_id))); |
| } |
| |
| void OnPasswordCredentialSelected( |
| const base::android::JavaRef<jobject>& jcallback, |
| std::u16string_view username, |
| std::u16string_view password) { |
| JNIEnv* env = AttachCurrentThread(); |
| base::android::RunObjectCallbackAndroid( |
| jcallback, Java_WebauthnBrowserBridge_createSelectedPasswordCredential( |
| env, ConvertUTF16ToJavaString(env, username), |
| ConvertUTF16ToJavaString(env, password))); |
| } |
| |
| void OnNonCredentialReturn(const base::android::JavaRef<jobject>& jcallback, |
| NonCredentialReturnReason return_reason) { |
| base::android::RunIntCallbackAndroid(jcallback, |
| static_cast<int32_t>(return_reason)); |
| } |
| |
| 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<jobjectArray>& credentials, |
| const base::android::JavaParamRef<jobject>& jframe_host, |
| jint mediation_type, |
| const base::android::JavaParamRef<jobject>& jcredential_callback, |
| const base::android::JavaParamRef<jobject>& jhybrid_callback, |
| const base::android::JavaParamRef<jobject>& jnon_credential_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)) { |
| base::android::RunIntCallbackAndroid( |
| jnon_credential_callback, |
| static_cast<int32_t>(NonCredentialReturnReason::kError)); |
| return; |
| } |
| |
| std::vector<device::DiscoverableCredentialMetadata> credentials_metadata; |
| ConvertJavaCredentialArrayToMetadataVector(env, credentials, |
| &credentials_metadata); |
| |
| CHECK(AssertionMediationType(mediation_type) != |
| AssertionMediationType::kImmediatePasskeysOnly || |
| !credentials_metadata.empty()); |
| |
| base::RepeatingCallback<void(const std::vector<uint8_t>&)> passkey_callback = |
| base::BindRepeating( |
| &OnWebauthnCredentialSelected, |
| ScopedJavaGlobalRef<jobject>(env, jcredential_callback)); |
| |
| base::RepeatingCallback<void(std::u16string_view, std::u16string_view)> |
| password_callback = base::BindRepeating( |
| &OnPasswordCredentialSelected, |
| ScopedJavaGlobalRef<jobject>(env, jcredential_callback)); |
| |
| base::RepeatingClosure hybrid_closure; |
| if (!jhybrid_callback.is_null()) { |
| hybrid_closure = base::BindRepeating( |
| &OnHybridAssertionInvoked, |
| ScopedJavaGlobalRef<jobject>(env, jhybrid_callback)); |
| } |
| |
| base::RepeatingCallback<void(NonCredentialReturnReason)> |
| non_credential_callback = base::BindRepeating( |
| &OnNonCredentialReturn, |
| ScopedJavaGlobalRef<jobject>(env, jnon_credential_callback)); |
| |
| client->OnWebAuthnRequestPending( |
| render_frame_host, std::move(credentials_metadata), |
| AssertionMediationType(mediation_type), std::move(passkey_callback), |
| std::move(password_callback), std::move(hybrid_closure), |
| std::move(non_credential_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 |