[go: nahoru, domu]

blob: a2ca8b28dd3ffc43667d82b587ebe863e059477a [file] [log] [blame]
// 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_array.h"
#include "base/android/jni_string.h"
#include "base/bind.h"
#include "base/callback.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/no_destructor.h"
#include "base/sequence_checker.h"
#include "base/timer/elapsed_timer.h"
#include "components/cbor/reader.h"
#include "components/cbor/values.h"
#include "components/cbor/writer.h"
#include "components/device_event_log/device_event_log.h"
#include "crypto/random.h"
#include "device/fido/cable/v2_authenticator.h"
#include "device/fido/cable/v2_handshake.h"
#include "device/fido/cable/v2_registration.h"
#include "device/fido/features.h"
#include "device/fido/fido_parsing_utils.h"
#include "third_party/blink/public/mojom/webauthn/authenticator.mojom.h"
#include "third_party/boringssl/src/include/openssl/bytestring.h"
#include "third_party/boringssl/src/include/openssl/ec.h"
#include "third_party/boringssl/src/include/openssl/mem.h"
#include "third_party/boringssl/src/include/openssl/obj.h"
// These "headers" actually contain several function definitions and thus can
// only be included once across Chromium.
#include "base/time/time.h"
#include "chrome/android/features/cablev2_authenticator/jni_headers/BLEAdvert_jni.h"
#include "chrome/android/features/cablev2_authenticator/jni_headers/CableAuthenticator_jni.h"
#include "chrome/android/features/cablev2_authenticator/jni_headers/USBHandler_jni.h"
using base::android::ConvertJavaStringToUTF8;
using base::android::ConvertUTF8ToJavaString;
using base::android::JavaByteArrayToByteVector;
using base::android::JavaParamRef;
using base::android::JavaRef;
using base::android::ScopedJavaGlobalRef;
using base::android::ScopedJavaLocalRef;
using base::android::ToJavaArrayOfByteArray;
using base::android::ToJavaByteArray;
using base::android::ToJavaIntArray;
namespace {
namespace protobuf {
// WireType enumerates the protobuf wire types. See
// https://developers.google.com/protocol-buffers/docs/encoding#structure
enum class WireType {
kVarint = 0,
kLengthPrefixed = 2,
};
// EncodeTag encodes a protobuf tag and type into the identifier value used on
// the wire. See
// https://developers.google.com/protocol-buffers/docs/encoding#structure
constexpr uint64_t EncodeTag(uint64_t tag_number, WireType type) {
return tag_number << 3 | static_cast<uint8_t>(type);
}
// Some tag numbers used in a protobuf that cannot be included here but which
// we wish to generate messages for.
constexpr uint64_t kTagRequestId = 1;
constexpr uint64_t kTagEventType = 2;
constexpr uint64_t kTagTunnelId = 11;
constexpr uint64_t kTagChromeLog = 501;
constexpr uint64_t kTagEvent = 1;
constexpr uint64_t kTagResult = 2;
// cbb_add_varint encodes |v| as a base128 varint as described at
// https://developers.google.com/protocol-buffers/docs/encoding#varints.
bool cbb_add_varint(CBB* cbb, uint64_t v) {
for (;;) {
const uint64_t next_v = v >> 7;
const bool is_last_byte = (next_v == 0);
const uint8_t b = (v & 0x7f) | (is_last_byte ? 0 : 0x80);
if (!CBB_add_u8(cbb, b)) {
return false;
}
if (is_last_byte) {
return true;
}
v = next_v;
}
}
enum class Type {
kEvent,
kResult,
};
// LogEvent logs an event on a server-linked transaction with the given
// |tunnel_id|. The semantics of |value| are specific to the |type| of the
// logged event.
void LogEvent(
JNIEnv* env,
base::span<const uint8_t, device::cablev2::kTunnelIdSize> tunnel_id,
Type type,
unsigned value) {
uint64_t tag;
switch (type) {
case Type::kEvent:
tag = kTagEvent;
break;
case Type::kResult:
tag = kTagResult;
break;
}
// An inner protobuf is serialised first in order to get its length for the
// outer message.
bssl::ScopedCBB inner_cbb;
uint8_t* inner_bytes;
size_t inner_length;
if (!CBB_init(inner_cbb.get(), 16) ||
!cbb_add_varint(inner_cbb.get(), EncodeTag(tag, WireType::kVarint)) ||
!cbb_add_varint(inner_cbb.get(), value) ||
!CBB_finish(inner_cbb.get(), &inner_bytes, &inner_length)) {
return;
}
bssl::UniquePtr<uint8_t> inner_bytes_storage(inner_bytes);
bssl::ScopedCBB cbb;
uint8_t* bytes;
size_t length;
if (!CBB_init(cbb.get(), 32) ||
// Protobuf entries are (tag, value) pairs.
!cbb_add_varint(cbb.get(), EncodeTag(kTagRequestId, WireType::kVarint)) ||
!cbb_add_varint(cbb.get(), 0) ||
!cbb_add_varint(cbb.get(), EncodeTag(kTagEventType, WireType::kVarint)) ||
!cbb_add_varint(cbb.get(), kTagChromeLog) ||
!cbb_add_varint(cbb.get(),
EncodeTag(kTagTunnelId, WireType::kLengthPrefixed)) ||
!cbb_add_varint(cbb.get(), tunnel_id.size()) ||
!CBB_add_bytes(cbb.get(), tunnel_id.data(), tunnel_id.size()) ||
!cbb_add_varint(cbb.get(),
EncodeTag(kTagChromeLog, WireType::kLengthPrefixed)) ||
!cbb_add_varint(cbb.get(), inner_length) ||
!CBB_add_bytes(cbb.get(), inner_bytes, inner_length) ||
!CBB_finish(cbb.get(), &bytes, &length)) {
return;
}
bssl::UniquePtr<uint8_t> bytes_storage(bytes);
Java_CableAuthenticator_logEvent(env, ToJavaByteArray(env, bytes, length));
}
} // namespace protobuf
// CableV2MobileEvent enumerates several steps that occur during a caBLEv2
// transaction. Do not change the assigned value since they are used in
// histograms, only append new values. Keep synced with enums.xml.
enum class CableV2MobileEvent {
kQRRead = 0,
kServerLink = 1,
kCloudMessage = 2,
kUSB = 3,
kSetup = 4,
kTunnelServerConnected = 5,
kHandshakeCompleted = 6,
kRequestReceived = 7,
kCTAPError = 8,
kUnlink = 9,
kNeedInteractive = 10,
kInteractionReady = 11,
kLinkingNotRequested = 12,
kUSBSuccess = 13,
kStoppedWhileAwaitingTunnelServerConnection = 14,
kStoppedWhileAwaitingHandshake = 15,
kStoppedWhileAwaitingRequest = 16,
kStoppedWhileAuthenticating = 17,
kStrayGetAssertionResponse = 18,
kGetAssertionStarted = 19,
kGetAssertionComplete = 20,
kFirstTransactionDone = 21,
kContactIDNotReady = 22,
kBluetoothAdvertisePermissionRequested = 23,
kBluetoothAdvertisePermissionGranted = 24,
kBluetoothAdvertisePermissionRejected = 25,
kMaxValue = 25,
};
// CableV2MobileResult enumerates the outcome of a caBLEv2 transction. Do not
// change the assigned value since they are used in histograms, only append new
// values. Keep synced with enums.xml.
enum class CableV2MobileResult {
kSuccess = 0,
kUnexpectedEOF = 1,
kTunnelServerConnectFailed = 2,
kHandshakeFailed = 3,
kDecryptFailure = 4,
kInvalidCBOR = 5,
kInvalidCTAP = 6,
kUnknownCommand = 7,
kInternalError = 8,
kInvalidQR = 9,
kInvalidServerLink = 10,
kEOFWhileProcessing = 11,
kDiscoverableCredentialsRejected = 12,
kMaxValue = 12,
};
// JavaByteArrayToSpan returns a span that aliases |data|. Be aware that the
// reference for |data| must outlive the span.
base::span<const uint8_t> JavaByteArrayToSpan(
JNIEnv* env,
const JavaParamRef<jbyteArray>& data) {
if (data.is_null()) {
return base::span<const uint8_t>();
}
const size_t data_len = env->GetArrayLength(data);
const jbyte* data_bytes = env->GetByteArrayElements(data, /*iscopy=*/nullptr);
return base::as_bytes(base::make_span(data_bytes, data_len));
}
// JavaByteArrayToFixedSpan returns a span that aliases |data|, or |nullopt| if
// the span is not of the correct length. Be aware that the reference for |data|
// must outlive the span.
template <size_t N>
absl::optional<base::span<const uint8_t, N>> JavaByteArrayToFixedSpan(
JNIEnv* env,
const JavaParamRef<jbyteArray>& data) {
static_assert(N != 0,
"Zero case is different from JavaByteArrayToSpan as null "
"inputs will always be rejected here.");
if (data.is_null()) {
return absl::nullopt;
}
const size_t data_len = env->GetArrayLength(data);
if (data_len != N) {
return absl::nullopt;
}
const jbyte* data_bytes = env->GetByteArrayElements(data, /*iscopy=*/nullptr);
return base::as_bytes(base::make_span<N>(data_bytes, data_len));
}
// GlobalData holds all the state for ongoing security key operations. Since
// there are ultimately only one human user, concurrent requests are not
// supported.
struct GlobalData {
JNIEnv* env = nullptr;
// instance_num is incremented for each new |Transaction| created and returned
// to Java to serve as a "handle". This prevents commands intended for a
// previous transaction getting applied to a replacement. The zero value is
// reserved so that functions can still return that to indicate an error.
jlong instance_num = 1;
// metrics_enabled records whether the user opted into metrics and crash
// reporting. If so then logging of events related to server-link
// transactions is permitted.
bool metrics_enabled = false;
absl::optional<std::array<uint8_t, device::cablev2::kRootSecretSize>>
root_secret;
network::mojom::NetworkContext* network_context = nullptr;
// event_to_record_if_stopped contains an event to record with UMA if the
// activity is stopped. This is updated as a transaction progresses.
absl::optional<CableV2MobileEvent> event_to_record_if_stopped;
// registration is a non-owning pointer to the global |Registration|.
device::cablev2::authenticator::Registration* registration = nullptr;
// current_transaction holds the |Transaction| that is currently active.
std::unique_ptr<device::cablev2::authenticator::Transaction>
current_transaction;
// pending_make_credential_callback holds the callback that the
// |Authenticator| expects to be run once a makeCredential operation has
// completed.
absl::optional<
device::cablev2::authenticator::Platform::MakeCredentialCallback>
pending_make_credential_callback;
// pending_get_assertion_callback holds the callback that the
// |Authenticator| expects to be run once a getAssertion operation has
// completed.
absl::optional<device::cablev2::authenticator::Platform::GetAssertionCallback>
pending_get_assertion_callback;
absl::optional<base::TimeTicks> get_assertion_start_time;
// usb_callback holds the callback that receives data from a USB connection.
absl::optional<
base::RepeatingCallback<void(absl::optional<base::span<const uint8_t>>)>>
usb_callback;
// server_link_tunnel_id contains the derived tunnel ID for server–link
// transactions. May be |nullopt| if the current transaction is not
// server-linked. This is used as an event ID when logging.
absl::optional<std::array<uint8_t, device::cablev2::kTunnelIdSize>>
server_link_tunnel_id;
};
// GetGlobalData returns a pointer to the unique |GlobalData| for the address
// space.
GlobalData& GetGlobalData() {
static base::NoDestructor<GlobalData> global_data;
return *global_data;
}
void ResetGlobalData() {
GlobalData& global_data = GetGlobalData();
global_data.metrics_enabled = false;
global_data.current_transaction.reset();
global_data.pending_make_credential_callback.reset();
global_data.pending_get_assertion_callback.reset();
global_data.get_assertion_start_time.reset();
global_data.usb_callback.reset();
global_data.server_link_tunnel_id.reset();
}
void RecordEvent(const GlobalData* global_data, CableV2MobileEvent event) {
base::UmaHistogramEnumeration("WebAuthentication.CableV2.MobileEvent", event);
if (global_data && global_data->metrics_enabled &&
global_data->server_link_tunnel_id.has_value()) {
protobuf::LogEvent(global_data->env, *global_data->server_link_tunnel_id,
protobuf::Type::kEvent, static_cast<unsigned>(event));
}
}
void RecordResult(const GlobalData* global_data, CableV2MobileResult result) {
base::UmaHistogramEnumeration("WebAuthentication.CableV2.MobileResult",
result);
if (global_data && global_data->metrics_enabled &&
global_data->server_link_tunnel_id.has_value()) {
protobuf::LogEvent(global_data->env, *global_data->server_link_tunnel_id,
protobuf::Type::kResult, static_cast<unsigned>(result));
}
}
// AndroidBLEAdvert wraps a Java |BLEAdvert| object so that
// |authenticator::Platform| can hold it.
class AndroidBLEAdvert
: public device::cablev2::authenticator::Platform::BLEAdvert {
public:
AndroidBLEAdvert(JNIEnv* env, ScopedJavaGlobalRef<jobject> advert)
: env_(env), advert_(std::move(advert)) {
DCHECK(env_->IsInstanceOf(
advert_.obj(),
org_chromium_chrome_browser_webauth_authenticator_BLEAdvert_clazz(
env)));
}
~AndroidBLEAdvert() override {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
Java_BLEAdvert_close(env_, advert_);
}
private:
const raw_ptr<JNIEnv> env_;
const ScopedJavaGlobalRef<jobject> advert_;
SEQUENCE_CHECKER(sequence_checker_);
};
// AndroidPlatform implements |authenticator::Platform| using the GMSCore
// implementation of FIDO operations.
class AndroidPlatform : public device::cablev2::authenticator::Platform {
public:
typedef base::OnceCallback<void(ScopedJavaGlobalRef<jobject>)>
InteractionReadyCallback;
typedef base::OnceCallback<void(InteractionReadyCallback)>
InteractionNeededCallback;
AndroidPlatform(JNIEnv* env,
const JavaRef<jobject>& cable_authenticator,
bool is_usb)
: env_(env), cable_authenticator_(cable_authenticator), is_usb_(is_usb) {
DCHECK(env_->IsInstanceOf(
cable_authenticator_.obj(),
org_chromium_chrome_browser_webauth_authenticator_CableAuthenticator_clazz(
env)));
}
~AndroidPlatform() override = default;
// Platform:
void MakeCredential(
blink::mojom::PublicKeyCredentialCreationOptionsPtr params,
MakeCredentialCallback callback) override {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
GlobalData& global_data = GetGlobalData();
DCHECK(!global_data.pending_make_credential_callback);
global_data.pending_make_credential_callback = std::move(callback);
std::vector<uint8_t> params_bytes =
blink::mojom::PublicKeyCredentialCreationOptions::Serialize(&params);
Java_CableAuthenticator_makeCredential(env_, cable_authenticator_,
ToJavaByteArray(env_, params_bytes));
}
void GetAssertion(blink::mojom::PublicKeyCredentialRequestOptionsPtr params,
GetAssertionCallback callback) override {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
GlobalData& global_data = GetGlobalData();
DCHECK(!global_data.pending_get_assertion_callback);
global_data.pending_get_assertion_callback = std::move(callback);
global_data.get_assertion_start_time = base::TimeTicks::Now();
std::vector<uint8_t> params_bytes =
blink::mojom::PublicKeyCredentialRequestOptions::Serialize(&params);
RecordEvent(&global_data, CableV2MobileEvent::kGetAssertionStarted);
Java_CableAuthenticator_getAssertion(env_, cable_authenticator_,
ToJavaByteArray(env_, params_bytes));
}
void OnStatus(Status status) override {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
LOG(ERROR) << __func__ << " " << static_cast<int>(status);
GlobalData& global_data = GetGlobalData();
CableV2MobileEvent event;
switch (status) {
case Status::TUNNEL_SERVER_CONNECT:
event = CableV2MobileEvent::kTunnelServerConnected;
tunnel_server_connect_time_.emplace();
global_data.event_to_record_if_stopped =
CableV2MobileEvent::kStoppedWhileAwaitingHandshake;
break;
case Status::HANDSHAKE_COMPLETE:
if (tunnel_server_connect_time_) {
base::UmaHistogramMediumTimes(
"WebAuthentication.CableV2.RendezvousTime",
tunnel_server_connect_time_->Elapsed());
tunnel_server_connect_time_.reset();
}
event = CableV2MobileEvent::kHandshakeCompleted;
global_data.event_to_record_if_stopped =
CableV2MobileEvent::kStoppedWhileAwaitingRequest;
break;
case Status::REQUEST_RECEIVED:
event = CableV2MobileEvent::kRequestReceived;
global_data.event_to_record_if_stopped =
CableV2MobileEvent::kStoppedWhileAuthenticating;
break;
case Status::CTAP_ERROR:
event = CableV2MobileEvent::kCTAPError;
break;
case Status::FIRST_TRANSACTION_DONE:
global_data.event_to_record_if_stopped.reset();
event = CableV2MobileEvent::kFirstTransactionDone;
break;
}
RecordEvent(&global_data, event);
if (!cable_authenticator_) {
return;
}
Java_CableAuthenticator_onStatus(env_, cable_authenticator_,
static_cast<int>(status));
}
void OnCompleted(absl::optional<Error> maybe_error) override {
LOG(ERROR) << __func__ << " "
<< (maybe_error ? static_cast<int>(*maybe_error) : -1);
GlobalData& global_data = GetGlobalData();
global_data.event_to_record_if_stopped.reset();
CableV2MobileResult result = CableV2MobileResult::kSuccess;
if (maybe_error) {
switch (*maybe_error) {
case Error::UNEXPECTED_EOF:
result = CableV2MobileResult::kUnexpectedEOF;
break;
case Error::EOF_WHILE_PROCESSING:
result = CableV2MobileResult::kEOFWhileProcessing;
break;
case Error::TUNNEL_SERVER_CONNECT_FAILED:
result = CableV2MobileResult::kTunnelServerConnectFailed;
break;
case Error::HANDSHAKE_FAILED:
result = CableV2MobileResult::kHandshakeFailed;
break;
case Error::DECRYPT_FAILURE:
result = CableV2MobileResult::kDecryptFailure;
break;
case Error::INVALID_CBOR:
result = CableV2MobileResult::kInvalidCBOR;
break;
case Error::INVALID_CTAP:
result = CableV2MobileResult::kInvalidCTAP;
break;
case Error::UNKNOWN_COMMAND:
result = CableV2MobileResult::kUnknownCommand;
break;
case Error::AUTHENTICATOR_SELECTION_RECEIVED:
case Error::DISCOVERABLE_CREDENTIALS_REQUEST:
result = CableV2MobileResult::kDiscoverableCredentialsRejected;
break;
case Error::INTERNAL_ERROR:
case Error::SERVER_LINK_WRONG_LENGTH:
case Error::SERVER_LINK_NOT_ON_CURVE:
case Error::NO_SCREENLOCK:
case Error::NO_BLUETOOTH_PERMISSION:
case Error::QR_URI_ERROR:
result = CableV2MobileResult::kInternalError;
break;
}
}
RecordResult(&global_data, result);
if (is_usb_ && result == CableV2MobileResult::kSuccess) {
RecordEvent(nullptr, CableV2MobileEvent::kUSBSuccess);
}
// The transaction might fail before interactive mode, thus
// |cable_authenticator_| may be empty.
if (cable_authenticator_) {
const bool ok = !maybe_error.has_value();
Java_CableAuthenticator_onComplete(
env_, cable_authenticator_, ok,
ok ? 0 : static_cast<int>(*maybe_error));
}
// ResetGlobalData will delete the |Transaction|, which will delete this
// object. Thus nothing else can be done after this call.
ResetGlobalData();
}
std::unique_ptr<BLEAdvert> SendBLEAdvert(
base::span<const uint8_t, device::cablev2::kAdvertSize> payload)
override {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return std::make_unique<AndroidBLEAdvert>(
env_, ScopedJavaGlobalRef<jobject>(Java_CableAuthenticator_newBLEAdvert(
env_, ToJavaByteArray(env_, payload))));
}
private:
const raw_ptr<JNIEnv> env_;
ScopedJavaGlobalRef<jobject> cable_authenticator_;
absl::optional<base::ElapsedTimer> tunnel_server_connect_time_;
// is_usb_ is true if this object was created in order to respond to a client
// connected over USB.
const bool is_usb_;
SEQUENCE_CHECKER(sequence_checker_);
base::WeakPtrFactory<AndroidPlatform> weak_factory_{this};
};
// USBTransport wraps the Java |USBHandler| object so that
// |authenticator::Platform| can use it as a transport.
class USBTransport : public device::cablev2::authenticator::Transport {
public:
USBTransport(JNIEnv* env, ScopedJavaGlobalRef<jobject> usb_device)
: env_(env), usb_device_(std::move(usb_device)) {
DCHECK(env_->IsInstanceOf(
usb_device_.obj(),
org_chromium_chrome_browser_webauth_authenticator_USBHandler_clazz(
env)));
}
~USBTransport() override { Java_USBHandler_close(env_, usb_device_); }
// GetCallback returns callback which will be called repeatedly with data from
// the USB connection, forwarded via the Java code.
base::RepeatingCallback<void(absl::optional<base::span<const uint8_t>>)>
GetCallback() {
return base::BindRepeating(&USBTransport::OnData,
weak_factory_.GetWeakPtr());
}
// Transport:
void StartReading(
base::RepeatingCallback<void(Update)> update_callback) override {
callback_ = update_callback;
Java_USBHandler_startReading(env_, usb_device_);
}
void Write(std::vector<uint8_t> data) override {
Java_USBHandler_write(env_, usb_device_, ToJavaByteArray(env_, data));
}
private:
void OnData(absl::optional<base::span<const uint8_t>> data) {
if (!data) {
callback_.Run(Disconnected::kDisconnected);
} else {
callback_.Run(device::fido_parsing_utils::Materialize(*data));
}
}
const raw_ptr<JNIEnv> env_;
const ScopedJavaGlobalRef<jobject> usb_device_;
base::RepeatingCallback<void(Update)> callback_;
base::WeakPtrFactory<USBTransport> weak_factory_{this};
};
} // anonymous namespace
// These functions are the entry points for CableAuthenticator.java and
// BLEHandler.java calling into C++.
static void JNI_CableAuthenticator_Setup(JNIEnv* env,
jlong registration_long,
jlong network_context_long,
const JavaParamRef<jbyteArray>& secret,
jboolean metrics_enabled) {
GlobalData& global_data = GetGlobalData();
global_data.metrics_enabled = metrics_enabled;
// The root_secret may not be provided when triggered for server-link. It
// won't be used in that case either, but we need to be able to grab it if
// setup() is called called for a different type of exchange.
base::span<const uint8_t> root_secret = JavaByteArrayToSpan(env, secret);
if (!root_secret.empty() && !global_data.root_secret) {
global_data.root_secret.emplace();
CHECK_EQ(global_data.root_secret->size(), root_secret.size());
memcpy(global_data.root_secret->data(), root_secret.data(),
global_data.root_secret->size());
}
// If starting a new transaction, don't record anything if stopped.
global_data.event_to_record_if_stopped.reset();
// This function can be called multiple times and must be idempotent. The
// |env| member of |global_data| is used to flag whether setup has
// already occurred.
if (global_data.env) {
return;
}
RecordEvent(&global_data, CableV2MobileEvent::kSetup);
global_data.env = env;
static_assert(sizeof(jlong) >= sizeof(void*), "");
global_data.registration =
reinterpret_cast<device::cablev2::authenticator::Registration*>(
registration_long);
global_data.registration->PrepareContactID();
global_data.network_context =
reinterpret_cast<network::mojom::NetworkContext*>(network_context_long);
}
static jlong JNI_CableAuthenticator_StartUSB(
JNIEnv* env,
const JavaParamRef<jobject>& cable_authenticator,
const JavaParamRef<jobject>& usb_device) {
GlobalData& global_data = GetGlobalData();
RecordEvent(&global_data, CableV2MobileEvent::kUSB);
auto transport = std::make_unique<USBTransport>(
env, ScopedJavaGlobalRef<jobject>(usb_device));
DCHECK(!global_data.usb_callback);
global_data.usb_callback = transport->GetCallback();
global_data.current_transaction =
device::cablev2::authenticator::TransactWithPlaintextTransport(
std::make_unique<AndroidPlatform>(env, cable_authenticator,
/*is_usb=*/true),
std::unique_ptr<device::cablev2::authenticator::Transport>(
transport.release()));
return ++global_data.instance_num;
}
static jlong JNI_CableAuthenticator_StartQR(
JNIEnv* env,
const JavaParamRef<jobject>& cable_authenticator,
const JavaParamRef<jstring>& authenticator_name,
const JavaParamRef<jstring>& qr_uri,
jboolean link) {
GlobalData& global_data = GetGlobalData();
RecordEvent(&global_data, CableV2MobileEvent::kQRRead);
const std::string& qr_string = ConvertJavaStringToUTF8(qr_uri);
absl::optional<device::cablev2::qr::Components> decoded_qr(
device::cablev2::qr::Parse(qr_string));
if (!decoded_qr) {
FIDO_LOG(ERROR) << "Failed to decode QR: " << qr_string;
RecordResult(&global_data, CableV2MobileResult::kInvalidQR);
return 0;
}
if (!link) {
RecordEvent(&global_data, CableV2MobileEvent::kLinkingNotRequested);
} else if (!global_data.registration->contact_id()) {
LOG(ERROR) << "Contact ID was not ready for QR transaction";
RecordEvent(&global_data, CableV2MobileEvent::kContactIDNotReady);
}
global_data.event_to_record_if_stopped =
CableV2MobileEvent::kStoppedWhileAwaitingTunnelServerConnection;
global_data
.current_transaction = device::cablev2::authenticator::TransactFromQRCode(
// Just because the client supports storing linking information doesn't
// imply that it supports revision one, but we happened to introduce
// these features at the same time.
/*protocol_revision=*/decoded_qr->supports_linking.has_value() ? 1 : 0,
std::make_unique<AndroidPlatform>(env, cable_authenticator,
/*is_usb=*/false),
global_data.network_context, *global_data.root_secret,
ConvertJavaStringToUTF8(authenticator_name), decoded_qr->secret,
decoded_qr->peer_identity,
link ? global_data.registration->contact_id() : absl::nullopt,
// If the QR code knows about at least two registered tunnel server
// domains then we consider it recent enough to use the new Crypter mode.
/*use_new_crypter_construction=*/decoded_qr->num_known_domains >= 2);
return ++global_data.instance_num;
}
std::tuple<base::span<const uint8_t, device::kP256X962Length>,
base::span<const uint8_t, device::cablev2::kQRSecretSize>,
std::array<uint8_t, device::cablev2::kTunnelIdSize>>
ParseServerLinkData(JNIEnv* env,
const JavaParamRef<jbyteArray>& server_link_data_java) {
constexpr size_t kDataSize =
device::kP256X962Length + device::cablev2::kQRSecretSize;
const absl::optional<base::span<const uint8_t, kDataSize>> server_link_data =
JavaByteArrayToFixedSpan<kDataSize>(env, server_link_data_java);
// validateServerLinkData should have been called to check this already.
CHECK(server_link_data);
const base::span<const uint8_t, device::kP256X962Length> peer_identity =
server_link_data->subspan<0, device::kP256X962Length>();
const base::span<const uint8_t, device::cablev2::kQRSecretSize> qr_secret =
server_link_data
->subspan<device::kP256X962Length, device::cablev2::kQRSecretSize>();
const std::array<uint8_t, device::cablev2::kTunnelIdSize> tunnel_id =
device::cablev2::Derive<device::cablev2::kTunnelIdSize>(
qr_secret, base::span<uint8_t>(),
device::cablev2::DerivedValueType::kTunnelID);
return std::make_tuple(peer_identity, qr_secret, tunnel_id);
}
static jlong JNI_CableAuthenticator_StartServerLink(
JNIEnv* env,
const JavaParamRef<jobject>& cable_authenticator,
const JavaParamRef<jbyteArray>& server_link_data_java) {
GlobalData& global_data = GetGlobalData();
auto server_link_values = ParseServerLinkData(env, server_link_data_java);
auto peer_identity = std::get<0>(server_link_values);
auto qr_secret = std::get<1>(server_link_values);
global_data.server_link_tunnel_id = std::get<2>(server_link_values);
// Sending pairing information is disabled when doing a server-linked
// connection, thus the root secret and authenticator name will not be used.
std::array<uint8_t, device::cablev2::kRootSecretSize> dummy_root_secret = {0};
std::string dummy_authenticator_name = "";
global_data.event_to_record_if_stopped =
CableV2MobileEvent::kStoppedWhileAwaitingTunnelServerConnection;
RecordEvent(&global_data, CableV2MobileEvent::kServerLink);
global_data.current_transaction =
device::cablev2::authenticator::TransactFromQRCode(
/*protocol_revision=*/0,
std::make_unique<AndroidPlatform>(env, cable_authenticator,
/*is_usb=*/false),
global_data.network_context, dummy_root_secret,
dummy_authenticator_name, qr_secret, peer_identity, absl::nullopt,
/*use_new_crypter_construction=*/false);
return ++global_data.instance_num;
}
static jlong JNI_CableAuthenticator_StartCloudMessage(
JNIEnv* env,
const JavaParamRef<jobject>& cable_authenticator,
const JavaParamRef<jbyteArray>& serialized_event) {
GlobalData& global_data = GetGlobalData();
RecordEvent(&global_data, CableV2MobileEvent::kCloudMessage);
auto event =
device::cablev2::authenticator::Registration::Event::FromSerialized(
JavaByteArrayToSpan(env, serialized_event));
if (!event) {
LOG(ERROR) << "Failed to parse event";
return 0;
}
DCHECK((event->source ==
device::cablev2::authenticator::Registration::Type::LINKING) ==
event->contact_id.has_value());
// There is deliberately no check for |!global_data.current_transaction|
// because multiple Cloud messages may come in from different paired devices.
// Only the most recent is processed.
global_data.event_to_record_if_stopped =
CableV2MobileEvent::kStoppedWhileAwaitingTunnelServerConnection;
global_data.current_transaction =
device::cablev2::authenticator::TransactFromFCM(
event->protocol_revision,
std::make_unique<AndroidPlatform>(env, cable_authenticator,
/*is_usb=*/false),
global_data.network_context, *global_data.root_secret,
event->routing_id, event->tunnel_id, event->pairing_id,
event->client_nonce, event->contact_id);
return ++global_data.instance_num;
}
static void JNI_CableAuthenticator_Stop(JNIEnv* env, jlong instance_num) {
GlobalData& global_data = GetGlobalData();
if (global_data.instance_num == instance_num) {
ResetGlobalData();
}
}
static int JNI_CableAuthenticator_ValidateServerLinkData(
JNIEnv* env,
const JavaParamRef<jbyteArray>& jdata) {
base::span<const uint8_t> data = JavaByteArrayToSpan(env, jdata);
if (data.size() != device::kP256X962Length + device::cablev2::kQRSecretSize) {
RecordResult(nullptr, CableV2MobileResult::kInvalidServerLink);
return static_cast<int>(device::cablev2::authenticator::Platform::Error::
SERVER_LINK_WRONG_LENGTH);
}
base::span<const uint8_t> x962 = data.subspan(0, device::kP256X962Length);
bssl::UniquePtr<EC_GROUP> p256(
EC_GROUP_new_by_curve_name(NID_X9_62_prime256v1));
bssl::UniquePtr<EC_POINT> point(EC_POINT_new(p256.get()));
if (!EC_POINT_oct2point(p256.get(), point.get(), x962.data(), x962.size(),
/*ctx=*/nullptr)) {
RecordResult(nullptr, CableV2MobileResult::kInvalidServerLink);
return static_cast<int>(device::cablev2::authenticator::Platform::Error::
SERVER_LINK_NOT_ON_CURVE);
}
return 0;
}
static int JNI_CableAuthenticator_ValidateQRURI(
JNIEnv* env,
const JavaParamRef<jstring>& qr_uri) {
const std::string& qr_string = ConvertJavaStringToUTF8(qr_uri);
if (!device::cablev2::qr::Parse(qr_string)) {
RecordResult(nullptr, CableV2MobileResult::kInvalidQR);
return static_cast<int>(
device::cablev2::authenticator::Platform::Error::QR_URI_ERROR);
}
return 0;
}
static void JNI_CableAuthenticator_OnActivityStop(JNIEnv* env,
jlong instance_num) {
GlobalData& global_data = GetGlobalData();
if (global_data.event_to_record_if_stopped &&
global_data.instance_num == instance_num) {
RecordEvent(&global_data, *global_data.event_to_record_if_stopped);
global_data.event_to_record_if_stopped.reset();
}
}
static void JNI_CableAuthenticator_OnAuthenticatorAttestationResponse(
JNIEnv* env,
jint ctap_status,
const JavaParamRef<jbyteArray>& jattestation_object) {
GlobalData& global_data = GetGlobalData();
if (!global_data.pending_make_credential_callback) {
return;
}
auto callback = std::move(*global_data.pending_make_credential_callback);
global_data.pending_make_credential_callback.reset();
std::move(callback).Run(ctap_status,
JavaByteArrayToSpan(env, jattestation_object));
}
static void JNI_CableAuthenticator_OnAuthenticatorAssertionResponse(
JNIEnv* env,
jint ctap_status,
const JavaParamRef<jbyteArray>& jresponse_bytes) {
GlobalData& global_data = GetGlobalData();
RecordEvent(&global_data, CableV2MobileEvent::kGetAssertionComplete);
// TODO: |get_assertion_start_time| should always be present in this case.
// But, at the time of writing, we are seeing some odd numbers in UMA metrics
// and aren't sure what's going on. Thus there's an if to avoid introducing
// a crash. If the number of records for this histogram are comparible to
// the number of recorded starts, then this can be removed.
DCHECK(global_data.get_assertion_start_time);
if (global_data.get_assertion_start_time) {
const base::TimeDelta duration =
base::TimeTicks::Now() - global_data.get_assertion_start_time.value();
base::UmaHistogramMediumTimes("WebAuthentication.CableV2.GetAssertionTime",
duration);
global_data.get_assertion_start_time.reset();
}
if (!global_data.pending_get_assertion_callback) {
RecordEvent(&global_data, CableV2MobileEvent::kStrayGetAssertionResponse);
return;
}
auto callback = std::move(*global_data.pending_get_assertion_callback);
global_data.pending_get_assertion_callback.reset();
if (ctap_status ==
static_cast<jint>(device::CtapDeviceResponseCode::kSuccess)) {
base::span<const uint8_t> response_bytes =
JavaByteArrayToSpan(env, jresponse_bytes);
auto response = blink::mojom::GetAssertionAuthenticatorResponse::New();
if (blink::mojom::GetAssertionAuthenticatorResponse::Deserialize(
response_bytes.data(), response_bytes.size(), &response)) {
std::move(callback).Run(ctap_status, std::move(response));
return;
}
ctap_status =
static_cast<jint>(device::CtapDeviceResponseCode::kCtap2ErrOther);
}
std::move(callback).Run(ctap_status, nullptr);
}
static void JNI_CableAuthenticator_RecordEvent(
JNIEnv* env,
jint event,
const JavaParamRef<jbyteArray>& server_link_data_java) {
auto server_link_values = ParseServerLinkData(env, server_link_data_java);
base::UmaHistogramEnumeration("WebAuthentication.CableV2.MobileEvent",
static_cast<CableV2MobileEvent>(event));
protobuf::LogEvent(env, /*tunnel_id=*/std::get<2>(server_link_values),
protobuf::Type::kEvent, event);
}
static void JNI_USBHandler_OnUSBData(JNIEnv* env,
const JavaParamRef<jbyteArray>& usb_data) {
GlobalData& global_data = GetGlobalData();
if (!global_data.usb_callback) {
return;
}
if (!usb_data) {
global_data.usb_callback->Run(absl::nullopt);
} else {
global_data.usb_callback->Run(JavaByteArrayToSpan(env, usb_data));
}
}