Commit 732df6a3 authored by Adam Langley's avatar Adam Langley Committed by Commit Bot

Add sketch of caBLE v2 authenticator support.

Not currently wired up from anywhere, full of TODOs, and not yet plumbed
into the GMS FIDO APIs and thus non-functional.

BUG=1002262

Change-Id: I65a01b6cf38ce9897dc83310f3af6c6b3c0c6e52
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2079613Reviewed-by: default avatarTed Choc <tedchoc@chromium.org>
Reviewed-by: default avatarSamuel Huang <huangs@chromium.org>
Reviewed-by: default avatarAndrew Grieve <agrieve@chromium.org>
Reviewed-by: default avatarNina Satragno <nsatragno@chromium.org>
Reviewed-by: default avatarMartin Kreichgauer <martinkr@google.com>
Commit-Queue: Adam Langley <agl@chromium.org>
Cr-Commit-Position: refs/heads/master@{#748329}
parent 50bae7c9
......@@ -250,6 +250,7 @@ android_library("chrome_java") {
"$google_play_services_package:google_play_services_vision_java",
"//base:base_java",
"//base:jni_java",
"//chrome/android/features/cablev2_authenticator:public_java",
"//chrome/android/features/keyboard_accessory:public_java",
"//chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/library:piet_resources",
"//chrome/android/modules/image_editor/provider:java",
......@@ -520,6 +521,7 @@ generate_product_config_srcjar("chrome_product_config") {
java_group("chrome_all_java") {
deps = [
":chrome_java",
"//chrome/android/features/cablev2_authenticator:internal_java",
"//chrome/android/features/keyboard_accessory:internal_java",
"//chrome/android/features/media_router:java",
"//chrome/android/features/test_dummy/internal:base_module_java",
......@@ -555,6 +557,7 @@ group("jni_headers") {
public_deps = [
":chrome_jni_headers",
"//chrome/android/features/autofill_assistant:jni_headers",
"//chrome/android/features/cablev2_authenticator/internal:jni_headers",
"//chrome/android/features/keyboard_accessory:jni_headers",
"//chrome/android/features/media_router:jni_headers",
"//chrome/android/public/profiles:jni_headers",
......
# 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.
import("//build/config/android/rules.gni")
import("//chrome/android/features/cablev2_authenticator/config.gni")
java_group("public_java") {
deps = [ "factory:public_java" ]
}
# Only chrome_all_java and test targets should depend on this internal target.
java_group("internal_java") {
deps = [ "factory:internal_java" ]
if (enable_android_cablev2_authenticator) {
deps += [ "internal:internal_java" ]
}
}
include_rules = [
"+components/cbor",
"+components/device_event_log",
"+device/fido",
"+third_party/boringssl",
"-content/public/android",
"+content/public/android/java/src/org/chromium/content_public",
]
file://device/fido/OWNERS
# TEAM: identity-dev@chromium.org
# COMPONENT: Blink>WebAuthentication
# 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.
import("//build/config/android/channel.gni")
declare_args() {
enable_android_cablev2_authenticator =
android_channel == "default" || android_channel == "canary"
}
# 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.
import("//build/config/android/rules.gni")
import("//chrome/android/features/android_library_factory_tmpl.gni")
import("//chrome/android/features/cablev2_authenticator/config.gni")
if (enable_android_cablev2_authenticator) {
_factory_sources = [ "java/src/org/chromium/chrome/browser/webauth/authenticator/CableAuthenticatorUIFactory.java" ]
} else {
_factory_sources = [ "dummy/java/src/org/chromium/chrome/browser/webauth/authenticator/CableAuthenticatorUIFactory.java" ]
}
android_library_factory("public_java") {
sources = _factory_sources
}
android_library("internal_java") {
deps = [
"//chrome/android/features/cablev2_authenticator/internal:internal_java",
"//third_party/android_deps:android_support_v7_appcompat_java",
]
sources = _factory_sources
}
// 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.
package org.chromium.chrome.browser.webauth.authenticator;
import android.support.v4.app.Fragment;
/**
* Use {@link #createComponent()} to instantiate a {@link ManualFillingComponent}.
*/
public class CableAuthenticatorUIFactory {
private CableAuthenticatorUIFactory() {}
/**
* Returns whether this feature is available in the current build.
*
* If false, no other methods of this class should be called.
*/
public static boolean isAvailable() {
return false;
}
/**
* Returns the class of {@link CableAuthenticatorUI}.
*/
public static Class<? extends Fragment> getFragmentClass() {
return null;
}
}
// 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.
package org.chromium.chrome.browser.webauth.authenticator;
import android.support.v4.app.Fragment;
/**
* Use {@link #createComponent()} to instantiate a {@link ManualFillingComponent}.
*/
public class CableAuthenticatorUIFactory {
private CableAuthenticatorUIFactory() {}
/**
* Returns whether this feature is available in the current build.
*
* If false, no other methods of this class should be called.
*/
public static boolean isAvailable() {
return true;
}
/**
* Returns the class of {@link CableAuthenticatorUI}.
*/
public static Class<? extends Fragment> getFragmentClass() {
return CableAuthenticatorUI.class;
}
}
# Copyright 2022 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.
import("//build/config/android/rules.gni")
import("//chrome/android/features/cablev2_authenticator/config.gni")
if (enable_android_cablev2_authenticator) {
android_library("internal_java") {
sources = [
"java/src/org/chromium/chrome/browser/webauth/authenticator/BLEHandler.java",
"java/src/org/chromium/chrome/browser/webauth/authenticator/CableAuthenticatorUI.java",
"java/src/org/chromium/chrome/browser/webauth/authenticator/CameraView.java",
"java/src/org/chromium/chrome/browser/webauth/authenticator/QRScanDialog.java",
]
deps = [
"$google_play_services_package:google_play_services_base_java",
"$google_play_services_package:google_play_services_vision_common_java",
"$google_play_services_package:google_play_services_vision_java",
"//base:base_java",
"//base:jni_java",
"//content/public/android:content_java",
"//third_party/android_deps:android_support_v7_appcompat_java",
"//third_party/android_deps:com_android_support_support_annotations_java",
"//ui/android:ui_full_java",
]
annotation_processor_deps = [ "//base/android/jni_generator:jni_processor" ]
}
generate_jni("jni_headers") {
sources = [ "java/src/org/chromium/chrome/browser/webauth/authenticator/BLEHandler.java" ]
}
source_set("native") {
sources = [ "native/cablev2_authenticator_android.cc" ]
deps = [
":jni_headers",
"//base",
"//components/cbor",
"//components/device_event_log",
"//content/public/browser",
"//crypto",
"//device/fido",
"//third_party/boringssl",
]
}
} else {
java_group("internal_java") {
deps = [
# This is needed to prevent a build-error. See comment in
# https://chromium-review.googlesource.com/c/chromium/src/+/2079613/12/chrome/android/features/cablev2_authenticator/internal/BUILD.gn#9
"//third_party/android_deps:com_android_support_support_annotations_java",
]
}
source_set("jni_headers") {
}
source_set("native") {
}
}
// 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.
package org.chromium.chrome.browser.webauth.authenticator;
import android.Manifest.permission;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import org.chromium.ui.base.ActivityAndroidPermissionDelegate;
import org.chromium.ui.base.AndroidPermissionDelegate;
import java.lang.ref.WeakReference;
/**
* A fragment that provides a UI for scanning caBLE v2 QR codes.
*/
public class CableAuthenticatorUI
extends Fragment implements OnClickListener, QRScanDialog.Callback {
private BLEHandler mBleHandler;
private AndroidPermissionDelegate mPermissionDelegate;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final Context context = getContext();
mBleHandler = new BLEHandler();
if (!mBleHandler.start()) {
// TODO: handle the case where exporting the GATT server fails.
}
mPermissionDelegate = new ActivityAndroidPermissionDelegate(
new WeakReference<Activity>((Activity) context));
}
@Override
@SuppressLint("SetTextI18n")
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
// Shows a placeholder UI that provides a button for scanning QR codes
// and a very basic animation for the rest of the screen.
// TODO: should check FEATURE_BLUETOOTH with
// https://developer.android.com/reference/android/content/pm/PackageManager.html#hasSystemFeature(java.lang.String)
final Context context = getContext();
Button button = new Button(context);
// TODO: strings should be translated but this will be replaced during
// the UI process.
button.setText("Scan QR code");
button.setOnClickListener(this);
LinearLayout layout = new LinearLayout(context);
layout.setOrientation(LinearLayout.VERTICAL);
layout.addView(button);
// A ProgressBar is included for now. This is in lieu of a future,
// real UI.
ProgressBar spinner = new ProgressBar(context);
spinner.setIndeterminate(true);
layout.addView(spinner);
return layout;
}
/**
* Called when the button to scan a QR code is pressed.
*/
@Override
public void onClick(View v) {
// If camera permission is already available, show the scanning
// dialog.
final Context context = getContext();
if (mPermissionDelegate.hasPermission(permission.CAMERA)) {
(new QRScanDialog(this)).show(getFragmentManager(), "dialog");
return;
}
// Otherwise prompt for permission first.
if (mPermissionDelegate.canRequestPermission(permission.CAMERA)) {
// The |Fragment| method |requestPermissions| is called rather than
// the method on |mPermissionDelegate| because the latter routes the
// |onRequestPermissionsResult| callback to the Activity, and not
// this fragment.
requestPermissions(new String[] {permission.CAMERA}, 1);
} else {
// TODO: permission cannot be requested on older versions of
// Android. Does Chrome always get camera permission at install
// time on those versions? If so, then this case should be
// impossible.
}
}
/**
* Called when the camera has scanned a FIDO QR code.
*/
@Override
public void onQRCode(String value) {
mBleHandler.onQRCode(value);
}
/**
* Called when camera permission has been requested and the user has resolved the permission
* request.
*/
@Override
public void onRequestPermissionsResult(
int requestCode, String[] permissions, int[] grantResults) {
mPermissionDelegate = null;
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
(new QRScanDialog(this)).show(getFragmentManager(), "dialog");
}
}
@Override
public void onDestroy() {
super.onDestroy();
mBleHandler.close();
}
}
// 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.
package org.chromium.chrome.browser.webauth.authenticator;
import android.content.Context;
import android.hardware.Camera;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import org.chromium.base.Log;
import java.io.IOException;
/**
* Provides a SurfaceView and adapts it for use as a camera preview target
* so that the current camera image can be displayed.
*/
class CameraView extends SurfaceView implements SurfaceHolder.Callback {
private static final String TAG = "CameraView";
private final Camera.PreviewCallback mCallback;
private Camera mCamera;
public CameraView(Context context, Camera.PreviewCallback callback) {
super(context);
mCallback = callback;
}
public void rearmCallback() {
if (mCamera != null) {
mCamera.setOneShotPreviewCallback(mCallback);
}
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
getHolder().addCallback(this);
// TODO: Camera.open is slow and shouldn't be called on the UI
// thread.
mCamera = Camera.open();
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
getHolder().removeCallback(this);
if (mCamera != null) {
mCamera.release();
mCamera = null;
}
}
private void startCamera() {
try {
mCamera.setPreviewDisplay(getHolder());
// Use a one-shot callback so that callbacks don't happen faster
// they're processed.
mCamera.setOneShotPreviewCallback(mCallback);
Camera.Parameters parameters = mCamera.getParameters();
parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
mCamera.setParameters(parameters);
// TODO: the display orientation should be configured as
// described in
// https://developer.android.com/reference/android/hardware/Camera.html#setDisplayOrientation(int)
mCamera.startPreview();
} catch (IOException e) {
Log.w(TAG, "Exception while starting camera", e);
}
}
private void stopCamera() {
if (mCamera == null) {
return;
}
mCamera.setOneShotPreviewCallback(null);
mCamera.stopPreview();
}
/** SurfaceHolder.Callback implementation. */
@Override
public void surfaceCreated(SurfaceHolder holder) {
startCamera();
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
stopCamera();
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
stopCamera();
startCamera();
}
}
// 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.
package org.chromium.chrome.browser.webauth.authenticator;
import android.content.DialogInterface;
import android.graphics.ImageFormat;
import android.hardware.Camera;
import android.os.Bundle;
import android.support.v4.app.DialogFragment;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.google.android.gms.vision.Frame;
import com.google.android.gms.vision.barcode.Barcode;
import com.google.android.gms.vision.barcode.BarcodeDetector;
import org.chromium.base.Log;
import org.chromium.base.ThreadUtils;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskTraits;
import org.chromium.content_public.browser.UiThreadTaskTraits;
import java.nio.ByteBuffer;
/**
* Displays a preview of what the default (rear) camera can see and processes images for QR
* codes. Closes once an applicable QR code has been found.
*/
public class QRScanDialog extends DialogFragment implements Camera.PreviewCallback {
/**
* FIDO QR codes begin with this prefix. This class will ignore QR codes that don't match
* this.
*/
public static final String FIDO_QR_PREFIX = "fido://c1/";
private static final String TAG = "QRScanDialog";
/**
* Receives a single call containing the decoded QR value. It will
* begin with FIDO_QR_PREFIX.
*/
public static interface Callback { void onQRCode(String value); }
private final Callback mCallback;
private BarcodeDetector mQRScanner;
private CameraView mCameraView;
private ByteBuffer mBuffer;
private boolean mDismissed;
QRScanDialog(Callback callback) {
super();
mCallback = callback;
}
@Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
mCameraView = new CameraView(getContext(), this);
return mCameraView;
}
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
if (mBuffer == null || mBuffer.capacity() < data.length) {
mBuffer = ByteBuffer.allocate(data.length);
} else {
mBuffer.clear();
}
mBuffer.put(data);
PostTask.postTask(TaskTraits.USER_VISIBLE_MAY_BLOCK,
() -> findQRCodes(mBuffer, camera.getParameters()));
}
@Override
public void onDismiss(DialogInterface dialog) {
mDismissed = true;
}
/**
* Potentially loads GmsCore modules for QR detection and performs QR detection on the image in
* {@link #mBuffer}. Runs on a background thread.
*/
private void findQRCodes(ByteBuffer buffer, Camera.Parameters cameraParams) {
if (mQRScanner == null) {
// This can trigger a load of GmsCore modules, which is too
// much to do on the main thread.
mQRScanner = new BarcodeDetector.Builder(getContext()).build();
if (mQRScanner == null) {
Log.i(TAG, "BarcodeDetector failed to load");
PostTask.postTask(UiThreadTaskTraits.DEFAULT, this::dismiss);
return;
}
}
// From
// https://developer.android.com/reference/android/hardware/Camera.PreviewCallback.html#onPreviewFrame(byte%5B%5D,%20android.hardware.Camera)
// "If Camera.Parameters.setPreviewFormat(int) is never called, the default will be
// the YCbCr_420_SP (NV21) format."
Frame frame = new Frame.Builder()
.setImageData(buffer, cameraParams.getPreviewSize().width,
cameraParams.getPreviewSize().height, ImageFormat.NV21)
.build();
SparseArray<Barcode> barcodes = mQRScanner.detect(frame);
PostTask.postTask(UiThreadTaskTraits.DEFAULT, () -> handleQRCodes(barcodes));
}
/**
* Handles the results of QR detection. Runs on the UI thread.
*/
private void handleQRCodes(SparseArray<Barcode> barcodes) {
ThreadUtils.assertOnUiThread();
// This dialog may have been dismissed while background QR detection was
// running.
if (mDismissed) {
return;
}
for (int i = 0; i < barcodes.size(); i++) {
String value = barcodes.valueAt(i).rawValue;
if (value.startsWith(FIDO_QR_PREFIX)) {
mCallback.onQRCode(value);
dismiss();
return;
}
}
mCameraView.rearmCallback();
}
}
......@@ -2974,6 +2974,7 @@ jumbo_static_library("browser") {
":explore_sites_proto",
":usage_stats_proto",
"//chrome/android:jni_headers",
"//chrome/android/features/cablev2_authenticator/internal:native",
"//chrome/android/modules/extra_icu/provider:native",
"//chrome/browser/android/thin_webview/internal",
"//chrome/browser/android/webapk:proto",
......@@ -2986,6 +2987,7 @@ jumbo_static_library("browser") {
"//chrome/browser/updates",
"//chrome/services/media_gallery_util/public/cpp",
"//components/autofill_assistant/browser",
"//components/cbor",
"//components/cdm/browser",
"//components/content_capture/android",
"//components/crash/android:crash_android",
......
......@@ -16,6 +16,7 @@ namespace device {
constexpr size_t kCableEphemeralIdSize = 16;
constexpr size_t kCableSessionPreKeySize = 32;
constexpr size_t kCableQRSecretSize = 16;
constexpr size_t kCableNonceSize = 8;
using CableEidArray = std::array<uint8_t, kCableEphemeralIdSize>;
using CableSessionPreKeyArray = std::array<uint8_t, kCableSessionPreKeySize>;
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment