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") { ...@@ -250,6 +250,7 @@ android_library("chrome_java") {
"$google_play_services_package:google_play_services_vision_java", "$google_play_services_package:google_play_services_vision_java",
"//base:base_java", "//base:base_java",
"//base:jni_java", "//base:jni_java",
"//chrome/android/features/cablev2_authenticator:public_java",
"//chrome/android/features/keyboard_accessory: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/feed/core/java/src/org/chromium/chrome/browser/feed/library:piet_resources",
"//chrome/android/modules/image_editor/provider:java", "//chrome/android/modules/image_editor/provider:java",
...@@ -520,6 +521,7 @@ generate_product_config_srcjar("chrome_product_config") { ...@@ -520,6 +521,7 @@ generate_product_config_srcjar("chrome_product_config") {
java_group("chrome_all_java") { java_group("chrome_all_java") {
deps = [ deps = [
":chrome_java", ":chrome_java",
"//chrome/android/features/cablev2_authenticator:internal_java",
"//chrome/android/features/keyboard_accessory:internal_java", "//chrome/android/features/keyboard_accessory:internal_java",
"//chrome/android/features/media_router:java", "//chrome/android/features/media_router:java",
"//chrome/android/features/test_dummy/internal:base_module_java", "//chrome/android/features/test_dummy/internal:base_module_java",
...@@ -555,6 +557,7 @@ group("jni_headers") { ...@@ -555,6 +557,7 @@ group("jni_headers") {
public_deps = [ public_deps = [
":chrome_jni_headers", ":chrome_jni_headers",
"//chrome/android/features/autofill_assistant: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/keyboard_accessory:jni_headers",
"//chrome/android/features/media_router:jni_headers", "//chrome/android/features/media_router:jni_headers",
"//chrome/android/public/profiles: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.annotation.TargetApi;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattServer;
import android.bluetooth.BluetoothGattServerCallback;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothManager;
import android.bluetooth.le.AdvertiseCallback;
import android.bluetooth.le.AdvertiseData;
import android.bluetooth.le.AdvertiseSettings;
import android.bluetooth.le.BluetoothLeAdvertiser;
import android.content.Context;
import android.os.ParcelUuid;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.annotations.CalledByNative;
import org.chromium.base.annotations.NativeMethods;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.SingleThreadTaskRunner;
import org.chromium.base.task.TaskTraits;
import java.io.Closeable;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.HashMap;
import java.util.UUID;
/**
* Interfaces the Android BLE APIs with C++ code in cablev2_authenticator_android.cc that implements
* the caBLE v2 protocol. There is, at most, a single instance of this class in an address space.
*/
@TargetApi(21)
class BLEHandler extends BluetoothGattServerCallback implements Closeable {
// TODO: remove @TargetApi once 21 is the minimum, Clank-wide.
private static final String TAG = "AuthenticatorBLEHandler";
// These UUIDs are taken from the FIDO spec[1], save for the caBLE UUID
// which is allocated to Google. See
// https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#ble
private static final String CABLE_UUID = "0000fde2-0000-1000-8000-00805f9b34fb";
private static final String CONTROL_POINT_UUID = "f1d0fff1-deaa-ecee-b42f-c9ba7ed623bb";
private static final String CONTROL_POINT_LENGTH_UUID = "f1d0fff3-deaa-ecee-b42f-c9ba7ed623bb";
private static final String SERVICE_REVISION_UUID = "00002a28-0000-1000-8000-00805f9b34fb";
private static final String STATUS_UUID = "f1d0fff2-deaa-ecee-b42f-c9ba7ed623bb";
private static final String CLIENT_CHARACTERISTIC_DESCRIPTOR_UUID =
"00002902-0000-1000-8000-00805f9b34fb";
// See
// https://www.bluetooth.com/wp-content/uploads/Sitecore-Media-Library/Gatt/Xml/Descriptors/org.bluetooth.descriptor.gatt.client_characteristic_configuration.xml
private static final int GATT_CCCD_NOTIFICATIONS_DISABLED = 0;
private static final int GATT_CCCD_NOTIFICATIONS_ENABLED = 1;
// TODO: delete, along with the helper function that uses it, once we no longer
// need to dump debugging information from this code.
private static final char[] HEX_CHARS = "0123456789ABCDEF".toCharArray();
/**
* The pending fragments to send to each client. If present, the value is
* never an empty array.
*/
private final HashMap<Long, byte[][]> mPendingFragments;
private final HashMap<Long, Integer> mKnownMtus;
/**
* Android's BLE callbacks may happen on arbitrary threads. In order to
* avoid dealing with concurrency here, and in the C++ code, BLE callbacks
* are bounced to a specified thread which is accessed via this task runner.
*/
private SingleThreadTaskRunner mTaskRunner;
private AdvertiseCallback mCallback;
private BluetoothGattServer mServer;
private BluetoothGattDescriptor mCccd;
private BluetoothGattCharacteristic mStatusChar;
BLEHandler() {
mPendingFragments = new HashMap<Long, byte[][]>();
mKnownMtus = new HashMap<Long, Integer>();
}
/**
* Exports the GATT service. Does not start advertising immediately, but calls the native
* start method, which may do so.
*
* @return true if successful and false on error.
*/
public boolean start() {
BluetoothGattService cableService = new BluetoothGattService(
UUID.fromString(CABLE_UUID), BluetoothGattService.SERVICE_TYPE_PRIMARY);
cableService.addCharacteristic(new BluetoothGattCharacteristic(
UUID.fromString(CONTROL_POINT_UUID), BluetoothGattCharacteristic.PROPERTY_WRITE,
BluetoothGattCharacteristic.PERMISSION_WRITE));
cableService.addCharacteristic(
new BluetoothGattCharacteristic(UUID.fromString(CONTROL_POINT_LENGTH_UUID),
BluetoothGattCharacteristic.PROPERTY_READ,
BluetoothGattCharacteristic.PERMISSION_READ));
mStatusChar = new BluetoothGattCharacteristic(UUID.fromString(STATUS_UUID),
BluetoothGattCharacteristic.PROPERTY_READ
| BluetoothGattCharacteristic.PROPERTY_NOTIFY,
BluetoothGattCharacteristic.PERMISSION_READ);
mCccd = new BluetoothGattDescriptor(UUID.fromString(CLIENT_CHARACTERISTIC_DESCRIPTOR_UUID),
BluetoothGattDescriptor.PERMISSION_WRITE | BluetoothGattDescriptor.PERMISSION_READ);
mStatusChar.addDescriptor(mCccd);
cableService.addCharacteristic(mStatusChar);
cableService.addCharacteristic(
new BluetoothGattCharacteristic(UUID.fromString(SERVICE_REVISION_UUID),
BluetoothGattCharacteristic.PROPERTY_READ
| BluetoothGattCharacteristic.PROPERTY_WRITE,
BluetoothGattCharacteristic.PERMISSION_READ
| BluetoothGattCharacteristic.PERMISSION_WRITE));
Context context = ContextUtils.getApplicationContext();
BluetoothManager manager =
(BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE);
mServer = manager.openGattServer(context, this);
if (!mServer.addService(cableService)) {
Log.i(TAG, "addService failed");
return false;
}
// Android does not document on which thread BLE callbacks will occur
// but, in practice they seem to happen on the Binder thread. In order
// to serialise callbacks, all processing (including native processing)
// happens on a dedicated thread. This is a SingleThreadTaskRunner so
// that all native callback happen in the same thread. That avoids
// worrying about JNIEnv objects and references being incorrectly used
// across threads.
assert mTaskRunner == null;
mTaskRunner = PostTask.createSingleThreadTaskRunner(TaskTraits.BEST_EFFORT);
mTaskRunner.postTask(() -> BLEHandlerJni.get().start(this));
return true;
}
@Override
public void onMtuChanged(BluetoothDevice device, int mtu) {
Long client = addressToLong(device.getAddress());
mTaskRunner.postTask(() -> {
mKnownMtus.put(client, mtu);
BLEHandlerJni.get().recordClientMtu(client, mtu);
});
}
@Override
public void onCharacteristicReadRequest(BluetoothDevice device, int requestId, int offset,
BluetoothGattCharacteristic characteristic) {
Log.i(TAG, "onCharacteristicReadRequest");
if (offset != 0) {
Log.i(TAG, "onCharacteristicReadRequest: non-zero offset request");
mServer.sendResponse(device, requestId, BluetoothGatt.GATT_FAILURE, 0, null);
return;
}
Long client = addressToLong(device.getAddress());
switch (characteristic.getUuid().toString()) {
case CONTROL_POINT_LENGTH_UUID:
mTaskRunner.postTask(() -> {
Integer mtu = mKnownMtus.get(client);
if (mtu == null) {
mtu = 512;
}
// TODO: should the MTU be trimmed to account for overhead?
byte[] mtuBytes = {(byte) (mtu >> 8), (byte) (mtu & 0xff)};
mServer.sendResponse(
device, requestId, BluetoothGatt.GATT_SUCCESS, 0, mtuBytes);
});
break;
default:
// The control-point length is the only characteristic that is
// read because the status sends data purely via notifications.
mServer.sendResponse(device, requestId, BluetoothGatt.GATT_FAILURE, 0, null);
}
}
@Override
public void onCharacteristicWriteRequest(BluetoothDevice device, int requestId,
BluetoothGattCharacteristic characteristic, boolean preparedWrite,
boolean responseNeeded, int offset, byte[] value) {
Log.i(TAG, "onCharacteristicWriteRequest");
if (value == null) {
return;
}
if (offset != 0) {
Log.i(TAG, "onCharacteristicWriteRequest: non-zero offset request");
if (responseNeeded) {
mServer.sendResponse(device, requestId, BluetoothGatt.GATT_FAILURE, 0, null);
}
return;
}
Long client = addressToLong(device.getAddress());
switch (characteristic.getUuid().toString()) {
case CONTROL_POINT_UUID:
// The buffer containing the data is owned by the BLE stack and
// might be reused once this callback returns. Thus a copy is
// made for the handler thread.
byte[] valueCopy = Arrays.copyOf(value, value.length);
mTaskRunner.postTask(() -> {
byte[][] responseFragments =
BLEHandlerJni.get().write(client.longValue(), valueCopy);
if (responseNeeded) {
int status = responseFragments == null ? BluetoothGatt.GATT_FAILURE
: BluetoothGatt.GATT_SUCCESS;
mServer.sendResponse(device, requestId, status, 0, null);
}
if (responseFragments != null && responseFragments.length > 0) {
Log.i(TAG,
"onCharacteristicWriteRequest sending "
+ hex(responseFragments[0]));
mStatusChar.setValue(responseFragments[0]);
mServer.notifyCharacteristicChanged(device, mStatusChar, /*confirm=*/false);
if (responseFragments.length > 1) {
mPendingFragments.put(client,
Arrays.copyOfRange(
responseFragments, 1, responseFragments.length));
}
}
});
break;
default:
// The control-point is the only characteristic that is written
// to as caBLE clients don't set the service revision to save
// a round-trip.
if (responseNeeded) {
mServer.sendResponse(device, requestId, BluetoothGatt.GATT_FAILURE, 0, null);
}
}
}
/**
* Called by the BLE stack when transmission of a notification has completed.
* If that transmission was successful, the next buffered fragment is sent the client.
*/
@Override
public void onNotificationSent(BluetoothDevice device, int status) {
Long client = addressToLong(device.getAddress());
mTaskRunner.postTask(() -> {
if (status != BluetoothGatt.GATT_SUCCESS) {
mPendingFragments.remove(client);
return;
}
byte[][] remainingFragments = mPendingFragments.get(client);
if (remainingFragments == null) {
return;
}
Log.i(TAG, "onNotificationSent sending " + hex(remainingFragments[0]));
mStatusChar.setValue(remainingFragments[0]);
mServer.notifyCharacteristicChanged(device, mStatusChar, /*confirm=*/false);
if (remainingFragments.length > 1) {
mPendingFragments.put(client,
Arrays.copyOfRange(remainingFragments, 1, remainingFragments.length));
} else {
mPendingFragments.remove(client);
}
});
}
private static String hex(byte[] bytes) {
char[] ret = new char[bytes.length * 2];
for (int j = 0; j < bytes.length; j++) {
int v = bytes[j] & 0xFF;
ret[j * 2] = HEX_CHARS[v >>> 4];
ret[j * 2 + 1] = HEX_CHARS[v & 0x0F];
}
return new String(ret);
}
// addressToLong converts a BLE address into a Long, which is smaller and
// easier to deal with.
private static Long addressToLong(String address) {
// See
// https://developer.android.com/reference/android/bluetooth/BluetoothDevice.html#getAddress()
// for the format of the address string.
return Long.valueOf(address.replace(":", ""), 16);
}
@Override
public void onDescriptorWriteRequest(BluetoothDevice device, int requestId,
BluetoothGattDescriptor descriptor, boolean preparedWrite, boolean responseNeeded,
int offset, byte[] value) {
// There is only a single descriptor: the CCCD used for notifications.
// It is how GATT clients subscribe to notifications and the protocol
// is part of the core GATT specification.
if (!descriptor.getUuid().equals(mCccd.getUuid())) {
return;
}
Log.i(TAG, "onDescriptorWriteRequest: " + hex(value) + " " + responseNeeded);
if (offset != 0 || value.length != 2) {
if (responseNeeded) {
mServer.sendResponse(device, requestId, BluetoothGatt.GATT_FAILURE, 0, null);
}
return;
}
int request = (int) value[0] + (int) value[1] * 256;
Long client = addressToLong(device.getAddress());
mTaskRunner.postTask(() -> {
int status = BluetoothGatt.GATT_FAILURE;
switch (request) {
case GATT_CCCD_NOTIFICATIONS_DISABLED:
mPendingFragments.remove(client);
status = BluetoothGatt.GATT_SUCCESS;
break;
case GATT_CCCD_NOTIFICATIONS_ENABLED:
status = BluetoothGatt.GATT_SUCCESS;
break;
}
if (responseNeeded) {
mServer.sendResponse(device, requestId, status, 0, null);
}
});
}
/**
* Called to indicate that a QR code was scanned by the user.
*
* @param value contents of the QR code, which will be a valid caBLE
* URL, i.e. "fido://c1/"...
*/
public void onQRCode(String value) {
mTaskRunner.postTask(() -> { BLEHandlerJni.get().onQRScanned(value); });
}
private void maybeStopAdvertising() {
if (mCallback == null) {
return;
}
BluetoothLeAdvertiser advertiser =
BluetoothAdapter.getDefaultAdapter().getBluetoothLeAdvertiser();
Log.i(TAG, "stopping advertising");
advertiser.stopAdvertising(mCallback);
mCallback = null;
}
@Override
public void close() {
mTaskRunner.postTask(() -> {
maybeStopAdvertising();
BLEHandlerJni.get().stop();
mTaskRunner.destroy();
});
}
/**
* Called by C++ code to start advertising a given UUID, which is passed
* as 16 bytes.
*/
@CalledByNative
public void sendBLEAdvert(byte[] dataUuidBytes) {
Log.i(TAG, "sendBLEAdvert " + dataUuidBytes.length);
maybeStopAdvertising();
BluetoothLeAdvertiser advertiser =
BluetoothAdapter.getDefaultAdapter().getBluetoothLeAdvertiser();
mCallback = new AdvertiseCallback() {
@Override
public void onStartFailure(int errorCode) {
Log.i(TAG, "advertising failure" + errorCode);
}
@Override
public void onStartSuccess(AdvertiseSettings settingsInEffect) {
Log.i(TAG, "advertising success");
}
};
AdvertiseSettings settings =
(new AdvertiseSettings.Builder())
.setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY)
.setConnectable(true)
.setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM)
.build();
ParcelUuid fidoUuid = new ParcelUuid(UUID.fromString(CABLE_UUID));
ByteBuffer bb = ByteBuffer.wrap(dataUuidBytes);
long high = bb.getLong();
long low = bb.getLong();
UUID dataUuid = new UUID(high, low);
AdvertiseData data = (new AdvertiseData.Builder())
.addServiceUuid(fidoUuid)
.addServiceUuid(new ParcelUuid(dataUuid))
.setIncludeDeviceName(false)
.setIncludeTxPowerLevel(false)
.build();
advertiser.startAdvertising(settings, data, mCallback);
}
@NativeMethods
interface Natives {
/**
* Called to alert the C++ code to a new instance. The C++ code calls back into this object
* to send data.
*/
void start(BLEHandler bleHandler);
void stop();
/**
* Called when a QR code has been scanned.
*
* @param value contents of the QR code, which will be a valid caBLE
* URL, i.e. "fido://c1/"...
*/
void onQRScanned(String value);
/**
* Called when the MTU of a client is learned.
*/
void recordClientMtu(long client, int mtu);
/**
* Called to alert the C++ code that a GATT client wrote data.
*/
byte[][] write(long client, byte[] data);
}
}
// 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();
}
}
// 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.
#include "base/android/jni_string.h"
#include "base/base64url.h"
#include "base/memory/singleton.h"
#include "base/numerics/safe_math.h"
#include "base/strings/string_number_conversions.h"
#include "base/task/post_task.h"
#include "components/cbor/reader.h"
#include "components/device_event_log/device_event_log.h"
#include "crypto/aead.h"
#include "device/fido/authenticator_get_info_response.h"
#include "device/fido/authenticator_supported_options.h"
#include "device/fido/cable/cable_discovery_data.h"
#include "device/fido/cable/v2_handshake.h"
#include "device/fido/fido_constants.h"
#include "third_party/boringssl/src/include/openssl/aes.h"
#include "third_party/boringssl/src/include/openssl/bytestring.h"
#include "third_party/boringssl/src/include/openssl/digest.h"
#include "third_party/boringssl/src/include/openssl/ec_key.h"
#include "third_party/boringssl/src/include/openssl/ecdh.h"
#include "third_party/boringssl/src/include/openssl/hkdf.h"
#include "third_party/boringssl/src/include/openssl/obj.h"
#include "third_party/boringssl/src/include/openssl/rand.h"
#include "third_party/boringssl/src/include/openssl/sha.h"
// This "header" is actually contains several function definitions and thus can
// only be included once across Chromium.
#include "chrome/android/features/cablev2_authenticator/internal/jni_headers/BLEHandler_jni.h"
namespace {
// Defragmenter accepts CTAP2 message fragments and reassembles them.
// See
// https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#ble-framing
class Defragmenter {
public:
// Process appends the fragment |in| to the current message. If there is an
// error, it returns false. Otherwise it returns true and, if a complete
// message is available, |*out_result| is set to the command value and payload
// and the Defragmenter is reset for the next message. Otherwise |*out_result|
// is empty.
//
// If this function returns false, the object is no longer usable for future
// fragments.
//
// The span in any |*out_result| value is only valid until the next call on
// this object and may alias |in|.
bool Process(base::span<const uint8_t> in,
base::Optional<std::pair<uint8_t, base::span<const uint8_t>>>*
out_result) {
CBS cbs;
CBS_init(&cbs, in.data(), in.size());
uint8_t lead_byte;
if (!CBS_get_u8(&cbs, &lead_byte)) {
return false;
}
const bool message_start = (lead_byte & 0x80) != 0;
if (message_start != expect_message_start_) {
return false;
}
if (message_start) {
// The most-significant bit isn't masked off in order to match up with
// the values in FidoBleDeviceCommand.
const uint8_t command = lead_byte;
uint16_t msg_len;
if (!CBS_get_u16(&cbs, &msg_len) || msg_len < CBS_len(&cbs)) {
return false;
}
if (msg_len == CBS_len(&cbs)) {
base::span<const uint8_t> span(CBS_data(&cbs), CBS_len(&cbs));
out_result->emplace(command, span);
return true;
}
expect_message_start_ = false;
command_ = command;
message_len_ = msg_len;
next_fragment_ = 0;
buf_.resize(0);
buf_.insert(buf_.end(), CBS_data(&cbs), CBS_data(&cbs) + CBS_len(&cbs));
out_result->reset();
return true;
}
if (next_fragment_ != lead_byte) {
return false;
}
buf_.insert(buf_.end(), CBS_data(&cbs), CBS_data(&cbs) + CBS_len(&cbs));
if (buf_.size() < message_len_) {
next_fragment_ = (next_fragment_ + 1) & 0x7f;
out_result->reset();
return true;
} else if (buf_.size() > message_len_) {
return false;
}
expect_message_start_ = true;
out_result->emplace(command_, buf_);
return true;
}
private:
std::vector<uint8_t> buf_;
uint8_t command_;
uint16_t message_len_;
uint8_t next_fragment_;
bool expect_message_start_ = true;
};
// AuthenticatorState contains the keys for a caBLE v2 authenticator.
struct AuthenticatorState {
// TODO: authenticator-global state isn't yet implemented.
base::Optional<std::pair<std::array<uint8_t, device::kCableNonceSize>,
std::array<uint8_t, device::kCableEphemeralIdSize>>>
pairing_nonce_and_eid;
base::Optional<std::pair<std::array<uint8_t, device::kCableNonceSize>,
std::array<uint8_t, device::kCableEphemeralIdSize>>>
qr_nonce_and_eid;
base::Optional<device::CableDiscoveryData> qr_discovery_data;
};
// Client represents the state of a single BLE peer.
class Client {
public:
Client(uint16_t mtu, const AuthenticatorState* auth_state)
: mtu_(mtu), auth_state_(auth_state) {}
bool Process(
base::span<const uint8_t> fragment,
base::Optional<std::vector<std::vector<uint8_t>>>* out_response) {
if (!ProcessImpl(fragment, out_response)) {
state_ = State::kError;
return false;
}
return true;
}
private:
enum State {
kHandshake,
kConnected,
kError,
};
bool ProcessImpl(
base::span<const uint8_t> fragment,
base::Optional<std::vector<std::vector<uint8_t>>>* out_response) {
out_response->reset();
if (state_ == State::kError) {
return false;
}
base::Optional<std::pair<uint8_t, base::span<const uint8_t>>> message;
if (!defrag_.Process(fragment, &message)) {
FIDO_LOG(ERROR) << "Failed to defragment message";
return false;
}
if (!message) {
return true;
}
std::vector<uint8_t> response;
switch (state_) {
case State::kHandshake: {
if (message->first !=
static_cast<uint8_t>(device::FidoBleDeviceCommand::kControl)) {
FIDO_LOG(ERROR) << "Expected control message but received command "
<< static_cast<unsigned>(message->first);
return false;
}
base::Optional<std::unique_ptr<device::cablev2::Crypter>>
handshake_result = device::cablev2::RespondToHandshake(
auth_state_->qr_discovery_data->v2->psk_gen_key,
auth_state_->qr_nonce_and_eid->first,
auth_state_->qr_nonce_and_eid->second, /*identity=*/nullptr,
/*pairing_data=*/nullptr, message->second, &response);
if (!handshake_result) {
FIDO_LOG(ERROR) << "Handshake failed";
return false;
}
crypter_ = std::move(handshake_result.value());
state_ = State::kConnected;
break;
}
case State::kConnected: {
if (message->first !=
static_cast<uint8_t>(device::FidoBleDeviceCommand::kMsg)) {
FIDO_LOG(ERROR) << "Expected normal message but received command "
<< static_cast<unsigned>(message->first);
return false;
}
std::vector<uint8_t> plaintext;
if (!crypter_->Decrypt(
static_cast<device::FidoBleDeviceCommand>(message->first),
message->second, &plaintext) ||
plaintext.empty()) {
FIDO_LOG(ERROR) << "Decryption failed";
return false;
}
base::span<const uint8_t> cbor_bytes = plaintext;
const auto command = cbor_bytes[0];
cbor_bytes = cbor_bytes.subspan(1);
base::Optional<cbor::Value> payload;
if (!cbor_bytes.empty()) {
payload = cbor::Reader::Read(cbor_bytes);
if (!payload) {
FIDO_LOG(ERROR)
<< "CBOR decoding failed for " << base::HexEncode(cbor_bytes);
return false;
}
}
switch (command) {
case static_cast<uint8_t>(
device::CtapRequestCommand::kAuthenticatorGetInfo): {
if (payload) {
FIDO_LOG(ERROR)
<< "getInfo command incorrectly contained payload";
return false;
}
std::array<uint8_t, device::kAaguidLength> aaguid{};
device::AuthenticatorGetInfoResponse get_info(
{device::ProtocolVersion::kCtap2}, aaguid);
// TODO: should be based on whether a screen-lock is enabled.
get_info.options.user_verification_availability =
device::AuthenticatorSupportedOptions::
UserVerificationAvailability::kSupportedAndConfigured;
response =
device::AuthenticatorGetInfoResponse::EncodeToCBOR(get_info);
response.insert(response.begin(), 0);
break;
}
case static_cast<uint8_t>(
device::CtapRequestCommand::kAuthenticatorMakeCredential): {
if (!payload) {
return false;
}
// TODO: display to GMSCore's WebAuthn API to handle this message.
return false;
}
default:
FIDO_LOG(ERROR) << "Received unknown command "
<< static_cast<unsigned>(command);
return false;
}
if (!crypter_->Encrypt(&response)) {
FIDO_LOG(ERROR) << "Failed to encrypt response";
return false;
}
break;
}
case State::kError:
NOTREACHED();
return false;
}
std::vector<std::vector<uint8_t>> fragments;
if (!Fragment(message->first, response, &fragments)) {
FIDO_LOG(ERROR) << "Failed to fragment response of length "
<< response.size();
return false;
}
out_response->emplace(std::move(fragments));
return true;
}
// Fragment takes a command value and payload and appends one of more
// fragments to |out_fragments| to respect |mtu_|. It returns true on success
// and false on error.
bool Fragment(uint8_t command,
base::span<const uint8_t> in,
std::vector<std::vector<uint8_t>>* out_fragments) {
DCHECK(command & 0x80);
if (in.size() > 0xffff || mtu_ < 4) {
return false;
}
const size_t max_initial_fragment_bytes = mtu_ - 3;
const size_t max_subsequent_fragment_bytes = mtu_ - 1;
std::vector<uint8_t> fragment = {command, (in.size() >> 8) & 0xff,
in.size() & 0xff};
const size_t todo = std::min(in.size(), max_initial_fragment_bytes);
fragment.insert(fragment.end(), in.data(), in.data() + todo);
in = in.subspan(todo);
out_fragments->emplace_back(std::move(fragment));
uint8_t frag_num = 0;
while (!in.empty()) {
fragment.clear();
fragment.reserve(mtu_);
fragment.push_back(frag_num);
frag_num = (frag_num + 1) & 0x7f;
const size_t todo = std::min(in.size(), max_subsequent_fragment_bytes);
fragment.insert(fragment.end(), in.data(), in.data() + todo);
in = in.subspan(todo);
out_fragments->emplace_back(std::move(fragment));
}
return true;
}
const uint16_t mtu_;
const AuthenticatorState* const auth_state_;
State state_ = State::kHandshake;
Defragmenter defrag_;
std::unique_ptr<device::cablev2::Crypter> crypter_;
};
// CableInterface is a singleton that receives events from BLEHandler.java:
// the code that interfaces to Android's BLE stack. All calls into this
// object happen on a single thread.
class CableInterface {
public:
static CableInterface* GetInstance() {
return base::Singleton<CableInterface>::get();
}
void Start(JNIEnv* env,
const base::android::JavaParamRef<jobject>& ble_handler) {
ble_handler_.Reset(ble_handler);
env_ = env;
}
void Stop() {
ble_handler_.Reset();
clients_.clear();
known_mtus_.clear();
auth_state_.qr_nonce_and_eid.reset();
auth_state_.pairing_nonce_and_eid.reset();
auth_state_.qr_discovery_data.reset();
env_ = nullptr;
}
void OnQRScanned(const std::string& qr_url) {
static const char kPrefix[] = "fido://c1/";
DCHECK(qr_url.find(kPrefix) == 0);
base::StringPiece qr_url_base64(qr_url);
qr_url_base64 = qr_url_base64.substr(sizeof(kPrefix) - 1);
std::string qr_secret_str;
if (!base::Base64UrlDecode(qr_url_base64,
base::Base64UrlDecodePolicy::DISALLOW_PADDING,
&qr_secret_str) ||
qr_secret_str.size() != device::kCableQRSecretSize) {
FIDO_LOG(ERROR) << "QR decoding failed: " << qr_url;
return;
}
uint8_t qr_secret[device::kCableQRSecretSize];
memcpy(qr_secret, qr_secret_str.data(), sizeof(qr_secret));
auth_state_.qr_discovery_data.emplace(qr_secret);
std::array<uint8_t, device::kCableNonceSize> nonce;
RAND_bytes(nonce.data(), nonce.size());
uint8_t eid_plaintext[AES_BLOCK_SIZE];
static_assert(sizeof(eid_plaintext) == AES_BLOCK_SIZE,
"EIDs are not AES blocks");
AES_KEY key;
CHECK(
AES_set_encrypt_key(
auth_state_.qr_discovery_data->v2->eid_gen_key.data(),
/*bits=*/8 * auth_state_.qr_discovery_data->v2->eid_gen_key.size(),
&key) == 0);
memcpy(eid_plaintext, nonce.data(), nonce.size());
memset(eid_plaintext + nonce.size(), 0,
sizeof(eid_plaintext) - nonce.size());
std::array<uint8_t, AES_BLOCK_SIZE> eid;
AES_encrypt(/*in=*/eid_plaintext, /*out=*/eid.data(), &key);
auth_state_.qr_nonce_and_eid.emplace(nonce, eid);
base::android::ScopedJavaLocalRef<jbyteArray> jbytes(
env_, env_->NewByteArray(sizeof(eid)));
env_->SetByteArrayRegion(jbytes.obj(), 0, eid.size(), (jbyte*)eid.data());
Java_BLEHandler_sendBLEAdvert(env_, ble_handler_, jbytes);
}
void RecordClientMTU(uint64_t client_adr, uint16_t mtu_bytes) {
known_mtus_.emplace(client_adr, mtu_bytes);
}
base::android::ScopedJavaLocalRef<jobjectArray> Write(
jlong client_addr,
const base::android::JavaParamRef<jbyteArray>& data) {
auto it = clients_.find(client_addr);
if (it == clients_.end()) {
DCHECK(known_mtus_.find(client_addr) != known_mtus_.end());
uint16_t mtu = known_mtus_[client_addr];
if (mtu == 0) {
mtu = 512;
}
it = clients_.emplace(client_addr, new Client(mtu, &auth_state_)).first;
}
Client* const client = it->second.get();
size_t data_len = env_->GetArrayLength(data);
jbyte* data_bytes = env_->GetByteArrayElements(data, nullptr);
base::Optional<std::vector<std::vector<uint8_t>>> response_fragments;
if (!client->Process(base::span<const uint8_t>(
reinterpret_cast<uint8_t*>(data_bytes), data_len),
&response_fragments)) {
return nullptr;
}
base::android::ScopedJavaLocalRef<jclass> byte_array_class(
env_, env_->FindClass("[B"));
if (!response_fragments) {
base::android::ScopedJavaLocalRef<jobjectArray> ret(
env_, env_->NewObjectArray(0, byte_array_class.obj(), nullptr));
return ret;
}
base::android::ScopedJavaLocalRef<jobjectArray> ret(
env_, env_->NewObjectArray(response_fragments->size(),
byte_array_class.obj(), nullptr));
for (size_t i = 0; i < response_fragments->size(); i++) {
const std::vector<uint8_t>& fragment = response_fragments->at(i);
base::android::ScopedJavaLocalRef<jbyteArray> jbytes(
env_, env_->NewByteArray(fragment.size()));
env_->SetByteArrayRegion(jbytes.obj(), 0, fragment.size(),
reinterpret_cast<const jbyte*>(fragment.data()));
env_->SetObjectArrayElement(ret.obj(), i, jbytes.obj());
}
return ret;
}
private:
friend struct base::DefaultSingletonTraits<CableInterface>;
CableInterface() = default;
JNIEnv* env_ = nullptr;
base::android::ScopedJavaGlobalRef<jobject> ble_handler_;
AuthenticatorState auth_state_;
std::map<uint64_t, uint16_t> known_mtus_;
std::map<uint64_t, std::unique_ptr<Client>> clients_;
};
} // anonymous namespace
// These functions are the entry points for BLEHandler.java calling into C++.
static void JNI_BLEHandler_Start(
JNIEnv* env,
const base::android::JavaParamRef<jobject>& ble_handler) {
CableInterface::GetInstance()->Start(env, ble_handler);
}
static void JNI_BLEHandler_Stop(JNIEnv* env) {
CableInterface::GetInstance()->Stop();
}
static void JNI_BLEHandler_OnQRScanned(
JNIEnv* env,
const base::android::JavaParamRef<jstring>& jvalue) {
CableInterface::GetInstance()->OnQRScanned(
base::android::ConvertJavaStringToUTF8(jvalue));
}
static void JNI_BLEHandler_RecordClientMtu(JNIEnv* env,
jlong client,
jint mtu_bytes) {
if (mtu_bytes > 0xffff) {
mtu_bytes = 0xffff;
}
CableInterface::GetInstance()->RecordClientMTU(client, mtu_bytes);
}
static base::android::ScopedJavaLocalRef<jobjectArray> JNI_BLEHandler_Write(
JNIEnv* env,
jlong client,
const base::android::JavaParamRef<jbyteArray>& data) {
return CableInterface::GetInstance()->Write(client, data);
}
...@@ -2974,6 +2974,7 @@ jumbo_static_library("browser") { ...@@ -2974,6 +2974,7 @@ jumbo_static_library("browser") {
":explore_sites_proto", ":explore_sites_proto",
":usage_stats_proto", ":usage_stats_proto",
"//chrome/android:jni_headers", "//chrome/android:jni_headers",
"//chrome/android/features/cablev2_authenticator/internal:native",
"//chrome/android/modules/extra_icu/provider:native", "//chrome/android/modules/extra_icu/provider:native",
"//chrome/browser/android/thin_webview/internal", "//chrome/browser/android/thin_webview/internal",
"//chrome/browser/android/webapk:proto", "//chrome/browser/android/webapk:proto",
...@@ -2986,6 +2987,7 @@ jumbo_static_library("browser") { ...@@ -2986,6 +2987,7 @@ jumbo_static_library("browser") {
"//chrome/browser/updates", "//chrome/browser/updates",
"//chrome/services/media_gallery_util/public/cpp", "//chrome/services/media_gallery_util/public/cpp",
"//components/autofill_assistant/browser", "//components/autofill_assistant/browser",
"//components/cbor",
"//components/cdm/browser", "//components/cdm/browser",
"//components/content_capture/android", "//components/content_capture/android",
"//components/crash/android:crash_android", "//components/crash/android:crash_android",
......
...@@ -16,6 +16,7 @@ namespace device { ...@@ -16,6 +16,7 @@ namespace device {
constexpr size_t kCableEphemeralIdSize = 16; constexpr size_t kCableEphemeralIdSize = 16;
constexpr size_t kCableSessionPreKeySize = 32; constexpr size_t kCableSessionPreKeySize = 32;
constexpr size_t kCableQRSecretSize = 16; constexpr size_t kCableQRSecretSize = 16;
constexpr size_t kCableNonceSize = 8;
using CableEidArray = std::array<uint8_t, kCableEphemeralIdSize>; using CableEidArray = std::array<uint8_t, kCableEphemeralIdSize>;
using CableSessionPreKeyArray = std::array<uint8_t, kCableSessionPreKeySize>; 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