| // Copyright 2020 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/webauthn/android/cable_module_android.h" |
| |
| #include "base/android/jni_array.h" |
| #include "base/base64.h" |
| #include "base/functional/bind.h" |
| #include "base/logging.h" |
| #include "base/no_destructor.h" |
| #include "base/sys_byteorder.h" |
| #include "base/task/task_traits.h" |
| #include "base/task/thread_pool.h" |
| #include "chrome/browser/browser_process.h" |
| #include "chrome/browser/gcm/instance_id/instance_id_profile_service_factory.h" |
| #include "chrome/browser/net/system_network_context_manager.h" |
| #include "chrome/browser/profiles/profile_manager.h" |
| #include "chrome/browser/sync/device_info_sync_service_factory.h" |
| #include "components/cbor/reader.h" |
| #include "components/cbor/values.h" |
| #include "components/gcm_driver/instance_id/instance_id_profile_service.h" |
| #include "components/prefs/pref_registry_simple.h" |
| #include "components/prefs/pref_service.h" |
| #include "components/sync_device_info/device_info.h" |
| #include "components/sync_device_info/device_info_sync_service.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "crypto/random.h" |
| #include "device/fido/cable/v2_constants.h" |
| #include "device/fido/cable/v2_handshake.h" |
| #include "device/fido/cable/v2_registration.h" |
| #include "device/fido/cbor_extract.h" |
| #include "device/fido/features.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.h" |
| #include "third_party/boringssl/src/include/openssl/ec_key.h" |
| #include "third_party/boringssl/src/include/openssl/hkdf.h" |
| #include "third_party/boringssl/src/include/openssl/mem.h" |
| #include "third_party/boringssl/src/include/openssl/obj.h" |
| |
| // These "headers" actually contains function definitions and thus can only be |
| // included once across Chromium. |
| #include "chrome/browser/webauthn/android/jni_headers/CableAuthenticatorModuleProvider_jni.h" |
| #include "chrome/browser/webauthn/android/jni_headers/PrivacySettingsFragment_jni.h" |
| |
| using device::cablev2::authenticator::Registration; |
| |
| namespace webauthn { |
| namespace authenticator { |
| |
| namespace { |
| |
| // kRootSecretPrefName is the name of a string preference that is kept in the |
| // browser's local state and which stores the base64-encoded root secret for |
| // the authenticator. |
| const char kRootSecretPrefName[] = "webauthn.authenticator_root_secret"; |
| |
| // RegistrationState is a singleton object that loads an install-wide secret at |
| // startup and holds two FCM registrations. One registration, the "linking" |
| // registration, is used when the user links with another device by scanning a |
| // QR code. The second is advertised via Sync for other devices signed into the |
| // same account. The reason for having two registrations is that the linking |
| // registration can be rotated if the user wishes to unlink all QR-linked |
| // devices. But we don't want to break synced peers when that happens. Instead, |
| // for synced peers we require that they have received a recent sync status from |
| // this device, i.e. we rotate them automatically. |
| class RegistrationState { |
| public: |
| void Register() { |
| DCHECK(!linking_registration_); |
| DCHECK(!sync_registration_); |
| DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); |
| |
| prelink_play_services_ = |
| base::FeatureList::IsEnabled(device::kWebAuthnPrelinkPlayServices); |
| |
| instance_id::InstanceIDDriver* const driver = |
| instance_id::InstanceIDProfileServiceFactory::GetForProfile( |
| g_browser_process->profile_manager()->GetPrimaryUserProfile()) |
| ->driver(); |
| linking_registration_ = device::cablev2::authenticator::Register( |
| driver, device::cablev2::authenticator::Registration::Type::LINKING, |
| base::BindOnce(&RegistrationState::OnLinkingRegistrationReady, |
| base::Unretained(this)), |
| base::BindRepeating(&RegistrationState::OnEvent, |
| base::Unretained(this))); |
| sync_registration_ = device::cablev2::authenticator::Register( |
| driver, device::cablev2::authenticator::Registration::Type::SYNC, |
| base::BindOnce(&RegistrationState::OnSyncRegistrationReady, |
| base::Unretained(this)), |
| base::BindRepeating(&RegistrationState::OnEvent, |
| base::Unretained(this))); |
| base::ThreadPool::PostTaskAndReplyWithResult( |
| FROM_HERE, {base::TaskPriority::BEST_EFFORT}, |
| base::BindOnce( |
| &RegistrationState::GetCanDeviceSupportCableOnBackgroundSequence), |
| base::BindOnce(&RegistrationState::OnDeviceSupportResult, |
| base::Unretained(this))); |
| |
| PrefService* const local_state = g_browser_process->local_state(); |
| std::string secret_base64 = local_state->GetString(kRootSecretPrefName); |
| |
| if (!secret_base64.empty()) { |
| std::string secret_str; |
| if (base::Base64Decode(secret_base64, &secret_str) && |
| secret_str.size() == secret_.size()) { |
| memcpy(secret_.data(), secret_str.data(), secret_.size()); |
| } else { |
| secret_base64.clear(); |
| } |
| } |
| |
| if (secret_base64.empty()) { |
| crypto::RandBytes(secret_); |
| local_state->SetString(kRootSecretPrefName, base::Base64Encode(secret_)); |
| } |
| |
| base::ThreadPool::PostTaskAndReplyWithResult( |
| FROM_HERE, {base::TaskPriority::BEST_EFFORT}, |
| base::BindOnce( |
| &RegistrationState::CalculateIdentityKeyOnBackgroundSequence, |
| secret_), |
| base::BindOnce(&RegistrationState::OnIdentityKeyReady, |
| base::Unretained(this))); |
| } |
| |
| bool is_registered_for_linking() const { |
| return linking_registration_ != nullptr; |
| } |
| bool is_registered_for_sync() const { return sync_registration_ != nullptr; } |
| |
| Registration* linking_registration() const { |
| return linking_registration_.get(); |
| } |
| |
| Registration* sync_registration() const { return sync_registration_.get(); } |
| |
| const std::array<uint8_t, 32>& secret() const { return secret_; } |
| |
| // have_data_for_sync returns true if this object has loaded enough state to |
| // put information into sync's DeviceInfo. |
| bool have_data_for_sync() const { |
| return device_supports_cable_.has_value() && identity_key_ && |
| sync_registration_ != nullptr && sync_registration_->contact_id() && |
| have_play_services_data(); |
| } |
| |
| const EC_KEY* identity_key() const { |
| DCHECK(identity_key_); |
| return identity_key_.get(); |
| } |
| |
| bool device_supports_cable() const { return *device_supports_cable_; } |
| bool prelink_play_services() const { return prelink_play_services_; } |
| |
| const absl::optional<std::vector<uint8_t>>& link_data_from_play_services() |
| const { |
| DCHECK(prelink_play_services_); |
| DCHECK(have_link_data_from_play_services_); |
| return link_data_from_play_services_; |
| } |
| |
| void SignalSyncWhenReady() { |
| if (sync_registration_ && !sync_registration_->contact_id()) { |
| sync_registration_->PrepareContactID(); |
| } |
| if (!have_play_services_data() && !play_services_query_pending_) { |
| QueryPlayServices(); |
| } |
| signal_sync_when_ready_ = true; |
| } |
| |
| void OnHavePlayServicesLinkingInformation( |
| absl::optional<std::vector<uint8_t>> cbor) { |
| DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); |
| DCHECK(play_services_query_pending_); |
| |
| play_services_query_pending_ = false; |
| link_data_from_play_services_ = std::move(cbor); |
| have_link_data_from_play_services_ = true; |
| link_data_from_play_services_timeticks_ = base::TimeTicks::Now(); |
| MaybeSignalSync(); |
| } |
| |
| private: |
| bool have_play_services_data() const { |
| DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); |
| |
| // If querying Play Services is disabled then it's always "ready". |
| if (!prelink_play_services_) { |
| return true; |
| } |
| // If there's no result, then we're not ready. |
| if (!have_link_data_from_play_services_) { |
| return false; |
| } |
| // If there's a query already pending then the result must be stale and |
| // there's nothing more to do here. |
| if (play_services_query_pending_) { |
| return false; |
| } |
| const base::TimeDelta staleness = |
| base::TimeTicks::Now() - link_data_from_play_services_timeticks_; |
| return staleness < base::Hours(12); |
| } |
| |
| void QueryPlayServices() { |
| DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); |
| DCHECK(!play_services_query_pending_); |
| |
| play_services_query_pending_ = true; |
| base::ThreadPool::PostTask( |
| FROM_HERE, {base::TaskPriority::BEST_EFFORT}, |
| base::BindOnce(&RegistrationState:: |
| GetPrelinkFromPlayServicesOnBackgroundSequence)); |
| } |
| |
| void OnLinkingRegistrationReady() { MaybeFlushPendingEvent(); } |
| |
| void OnSyncRegistrationReady() { MaybeSignalSync(); } |
| |
| // OnEvent is called when a GCM message is received. |
| void OnEvent(std::unique_ptr<Registration::Event> event) { |
| DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); |
| |
| pending_event_ = std::move(event); |
| |
| MaybeFlushPendingEvent(); |
| } |
| |
| void MaybeFlushPendingEvent() { |
| if (!pending_event_) { |
| return; |
| } |
| |
| if (pending_event_->source == Registration::Type::LINKING && |
| !pending_event_->contact_id) { |
| // This GCM message is from a QR-linked peer so it needs the contact ID |
| // to be processed. |
| pending_event_->contact_id = linking_registration_->contact_id(); |
| |
| if (!pending_event_->contact_id) { |
| // The contact ID isn't ready yet. Wait until it is. |
| linking_registration_->PrepareContactID(); |
| return; |
| } |
| } |
| |
| std::unique_ptr<Registration::Event> event(std::move(pending_event_)); |
| if (event->source == Registration::Type::SYNC) { |
| // If this is from a synced peer then we limit how old the keys can be. |
| // Clank will update its device information once per day (when launched) |
| // and we piggyback on that to transmit fresh keys. Therefore syncing |
| // peers should have reasonably recent information. |
| uint64_t id; |
| static_assert(EXTENT(event->pairing_id) == sizeof(id), ""); |
| memcpy(&id, event->pairing_id.data(), sizeof(id)); |
| |
| // A maximum age is enforced for sync secrets so that any leak of |
| // information isn't valid forever. The desktop ignores DeviceInfo |
| // records with information that is too old so this should never happen |
| // with honest clients. |
| if (id > std::numeric_limits<uint32_t>::max() || |
| device::cablev2::sync::IDIsMoreThanNPeriodsOld( |
| static_cast<uint32_t>(id), |
| device::cablev2::kMaxSyncInfoDaysForProducer)) { |
| LOG(ERROR) << "Pairing ID " << id << " is too old. Dropping."; |
| return; |
| } |
| } |
| |
| const absl::optional<std::vector<uint8_t>> serialized(event->Serialize()); |
| if (!serialized) { |
| return; |
| } |
| |
| JNIEnv* const env = base::android::AttachCurrentThread(); |
| Java_CableAuthenticatorModuleProvider_onCloudMessage( |
| env, base::android::ToJavaByteArray(env, *serialized), |
| event->request_type == device::FidoRequestType::kMakeCredential); |
| } |
| |
| // MaybeSignalSync prompts the Sync system to refresh local-device data if |
| // the Sync data is now ready and |signal_sync_when_ready_| has been set to |
| // indicate that the Sync data was not available last time Sync queried it. |
| void MaybeSignalSync() { |
| if (!signal_sync_when_ready_ || !have_data_for_sync()) { |
| return; |
| } |
| signal_sync_when_ready_ = false; |
| |
| DeviceInfoSyncServiceFactory::GetForProfile( |
| ProfileManager::GetPrimaryUserProfile()) |
| ->RefreshLocalDeviceInfo(); |
| } |
| |
| static bool GetCanDeviceSupportCableOnBackgroundSequence() { |
| // This runs on a worker thread because this Java function can take a |
| // little while and it shouldn't block the UI thread. |
| return Java_CableAuthenticatorModuleProvider_canDeviceSupportCable( |
| base::android::AttachCurrentThread()); |
| } |
| |
| static void GetPrelinkFromPlayServicesOnBackgroundSequence() { |
| // This runs on a worker thread because this Java function can take a |
| // little while and it shouldn't block the UI thread. |
| Java_CableAuthenticatorModuleProvider_getLinkingInformation( |
| base::android::AttachCurrentThread()); |
| } |
| |
| // OnCanDeviceSupportCable is run with the result of `TestDeviceSupport`. |
| void OnDeviceSupportResult(bool result) { |
| DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); |
| |
| device_supports_cable_ = result; |
| MaybeSignalSync(); |
| } |
| |
| static bssl::UniquePtr<EC_KEY> CalculateIdentityKeyOnBackgroundSequence( |
| std::array<uint8_t, 32> secret) { |
| // This runs on a worker thread because the scalar multiplication takes a |
| // few milliseconds on slower devices. |
| return device::cablev2::IdentityKey(secret); |
| } |
| |
| // OnIdentityKeyReady is run with the result of `CalculateIdentityKey`. |
| void OnIdentityKeyReady(bssl::UniquePtr<EC_KEY> identity_key) { |
| DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); |
| |
| identity_key_ = std::move(identity_key); |
| MaybeSignalSync(); |
| } |
| |
| std::unique_ptr<Registration> linking_registration_; |
| std::unique_ptr<Registration> sync_registration_; |
| std::array<uint8_t, 32> secret_; |
| // identity_key_ is a public/private P-256 key that is calculated from |
| // `secret_`. It's cached because it takes some time to compute. |
| bssl::UniquePtr<EC_KEY> identity_key_; |
| std::unique_ptr<Registration::Event> pending_event_; |
| // device_supports_cable_ caches the result of a Java function that checks |
| // some prerequisites: that the device has Bluetooth and a screenlock. If |
| // this value is |nullopt| then its value has not yet been determined. |
| // |
| // The presence of a screen lock could change but, because of this caching, |
| // Clank won't notice in this context until the process restarts. Users can |
| // always use a QR code if pre-linking hasn't worked by the time they need |
| // it. |
| absl::optional<bool> device_supports_cable_; |
| // link_data_from_play_services_ contains the response from Play Services, as |
| // CBOR-encoded linking information, or `nullopt` if the call was |
| // unsuccessful. This field is only meaningful if |
| // `have_link_data_from_play_services_` is true. |
| absl::optional<std::vector<uint8_t>> link_data_from_play_services_; |
| // have_link_data_from_play_services_ is true if any call to Play Services has |
| // ever completed, successful or not. |
| bool have_link_data_from_play_services_ = false; |
| // link_data_from_play_services_timeticks_ contains the timestamp when |
| // `link_data_from_play_services_` was set. |
| base::TimeTicks link_data_from_play_services_timeticks_; |
| // play_services_query_pending_ is true if a request to Play Services is |
| // currently outstanding. |
| bool play_services_query_pending_ = false; |
| bool signal_sync_when_ready_ = false; |
| // prelink_play_services_ records the value of the feature flag |
| // `kWebAuthnPrelinkPlayServices`. It's recorded here because its value could |
| // change at run-time, but this code doesn't handle that. |
| bool prelink_play_services_ = false; |
| }; |
| |
| RegistrationState* GetRegistrationState() { |
| static base::NoDestructor<RegistrationState> state; |
| return state.get(); |
| } |
| |
| using device::cbor_extract::IntKey; |
| using device::cbor_extract::Is; |
| using device::cbor_extract::Map; |
| using device::cbor_extract::StepOrByte; |
| using device::cbor_extract::Stop; |
| |
| // PreLinkInfo reflects the linking information provided by Play Services. |
| struct PreLinkInfo { |
| // All fields below are not a raw_ptr<T> because cbor_extract.cc would |
| // cast the raw_ptr<T> to a void*, skipping an AddRef() call and causing a |
| // ref-counting mismatch. |
| RAW_PTR_EXCLUSION const std::vector<uint8_t>* contact_id; |
| RAW_PTR_EXCLUSION const std::vector<uint8_t>* pairing_id; |
| RAW_PTR_EXCLUSION const std::vector<uint8_t>* secret; |
| RAW_PTR_EXCLUSION const std::vector<uint8_t>* peer_public_key_x962; |
| }; |
| |
| // kPreLinkInfoSteps contains parsing instructions for cbor_extract to convert |
| // the CBOR-encoded data from Play Services into a `PreLinkInfo`. The format |
| // that Play Services uses mostly follows the "linking map" structure defined in |
| // https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#hybrid-qr-initiated |
| static constexpr StepOrByte<PreLinkInfo> kPreLinkInfoSteps[] = { |
| // clang-format off |
| ELEMENT(Is::kRequired, PreLinkInfo, contact_id), |
| IntKey<PreLinkInfo>(1), |
| ELEMENT(Is::kRequired, PreLinkInfo, pairing_id), |
| IntKey<PreLinkInfo>(2), |
| ELEMENT(Is::kRequired, PreLinkInfo, secret), |
| IntKey<PreLinkInfo>(3), |
| ELEMENT(Is::kRequired, PreLinkInfo, peer_public_key_x962), |
| IntKey<PreLinkInfo>(4), |
| |
| Stop<PreLinkInfo>(), |
| // clang-format on |
| }; |
| |
| } // namespace |
| |
| namespace internal { |
| |
| absl::optional<syncer::DeviceInfo::PhoneAsASecurityKeyInfo> PaaskInfoFromCBOR( |
| base::span<const uint8_t> cbor) { |
| absl::optional<cbor::Value> value = cbor::Reader::Read(cbor); |
| if (!value || !value->is_map()) { |
| return absl::nullopt; |
| } |
| |
| PreLinkInfo info; |
| uint64_t pairing_id; |
| std::array<uint8_t, 32> secret; |
| std::array<uint8_t, 65> peer_public_key_x962; |
| if (!device::cbor_extract::Extract<PreLinkInfo>(&info, kPreLinkInfoSteps, |
| value->GetMap()) || |
| info.pairing_id->size() != sizeof(pairing_id) || |
| info.secret->size() != secret.size() || |
| info.peer_public_key_x962->size() != peer_public_key_x962.size()) { |
| return absl::nullopt; |
| } |
| memcpy(&pairing_id, info.pairing_id->data(), sizeof(pairing_id)); |
| |
| memcpy(secret.data(), info.secret->data(), secret.size()); |
| memcpy(peer_public_key_x962.data(), info.peer_public_key_x962->data(), |
| peer_public_key_x962.size()); |
| |
| syncer::DeviceInfo::PhoneAsASecurityKeyInfo paask_info; |
| paask_info.tunnel_server_domain = device::cablev2::kTunnelServer.value(); |
| paask_info.contact_id = std::move(*info.contact_id); |
| if (pairing_id > std::numeric_limits<uint32_t>::max()) { |
| return absl::nullopt; |
| } |
| paask_info.id = static_cast<uint32_t>(pairing_id); |
| paask_info.secret = secret; |
| paask_info.peer_public_key_x962 = peer_public_key_x962; |
| return paask_info; |
| } |
| |
| } // namespace internal |
| |
| void RegisterForCloudMessages() { |
| DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); |
| |
| GetRegistrationState()->Register(); |
| } |
| |
| void RegisterLocalState(PrefRegistrySimple* registry) { |
| registry->RegisterStringPref(kRootSecretPrefName, std::string()); |
| } |
| |
| absl::optional<syncer::DeviceInfo::PhoneAsASecurityKeyInfo> |
| GetSyncDataIfRegistered() { |
| DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); |
| |
| RegistrationState* state = GetRegistrationState(); |
| if (!state->have_data_for_sync()) { |
| // Not yet ready to provide sync data. When the data is ready, |
| // |state| will signal to Sync that something changed and this |
| // function will be called again. |
| state->SignalSyncWhenReady(); |
| return absl::nullopt; |
| } |
| |
| if (state->prelink_play_services() && state->link_data_from_play_services()) { |
| absl::optional<syncer::DeviceInfo::PhoneAsASecurityKeyInfo> paask_info = |
| internal::PaaskInfoFromCBOR(*state->link_data_from_play_services()); |
| if (paask_info) { |
| return *paask_info; |
| } else { |
| LOG(ERROR) |
| << "Failed to parse PaaSK prelink information from Play Services"; |
| } |
| } |
| |
| if (!state->device_supports_cable()) { |
| return absl::nullopt; |
| } |
| |
| syncer::DeviceInfo::PhoneAsASecurityKeyInfo paask_info; |
| paask_info.tunnel_server_domain = device::cablev2::kTunnelServer.value(); |
| paask_info.contact_id = *state->sync_registration()->contact_id(); |
| const uint32_t pairing_id = device::cablev2::sync::IDNow(); |
| paask_info.id = pairing_id; |
| |
| std::array<uint8_t, device::cablev2::kPairingIDSize> pairing_id_bytes = {0}; |
| static_assert(sizeof(pairing_id) <= EXTENT(pairing_id_bytes), ""); |
| memcpy(pairing_id_bytes.data(), &pairing_id, sizeof(pairing_id)); |
| |
| paask_info.secret = device::cablev2::Derive<EXTENT(paask_info.secret)>( |
| state->secret(), pairing_id_bytes, |
| device::cablev2::DerivedValueType::kPairedSecret); |
| |
| CHECK_EQ(paask_info.peer_public_key_x962.size(), |
| EC_POINT_point2oct(EC_KEY_get0_group(state->identity_key()), |
| EC_KEY_get0_public_key(state->identity_key()), |
| POINT_CONVERSION_UNCOMPRESSED, |
| paask_info.peer_public_key_x962.data(), |
| paask_info.peer_public_key_x962.size(), |
| /*ctx=*/nullptr)); |
| |
| return paask_info; |
| } |
| |
| } // namespace authenticator |
| } // namespace webauthn |
| |
| // JNI callbacks. |
| |
| static jlong JNI_CableAuthenticatorModuleProvider_GetSystemNetworkContext( |
| JNIEnv* env) { |
| static_assert(sizeof(jlong) >= sizeof(uintptr_t), |
| "Java longs are too small to contain pointers"); |
| return static_cast<jlong>(reinterpret_cast<uintptr_t>( |
| SystemNetworkContextManager::GetInstance()->GetContext())); |
| } |
| |
| static jlong JNI_CableAuthenticatorModuleProvider_GetRegistration(JNIEnv* env) { |
| static_assert(sizeof(jlong) >= sizeof(uintptr_t), |
| "Java longs are too small to contain pointers"); |
| return static_cast<jlong>(reinterpret_cast<uintptr_t>( |
| webauthn::authenticator::GetRegistrationState()->linking_registration())); |
| } |
| |
| static void JNI_CableAuthenticatorModuleProvider_FreeEvent(JNIEnv* env, |
| jlong event_long) { |
| static_assert(sizeof(jlong) >= sizeof(uintptr_t), |
| "Java longs are too small to contain pointers"); |
| Registration::Event* event = |
| reinterpret_cast<Registration::Event*>(event_long); |
| delete event; |
| } |
| |
| static base::android::ScopedJavaLocalRef<jbyteArray> |
| JNI_CableAuthenticatorModuleProvider_GetSecret(JNIEnv* env) { |
| return base::android::ToJavaByteArray( |
| env, webauthn::authenticator::GetRegistrationState()->secret()); |
| } |
| |
| static void OnHavePlayServicesLinkingInformation( |
| absl::optional<std::vector<uint8_t>> cbor) { |
| DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); |
| webauthn::authenticator::GetRegistrationState() |
| ->OnHavePlayServicesLinkingInformation(std::move(cbor)); |
| } |
| |
| static void JNI_CableAuthenticatorModuleProvider_OnHaveLinkingInformation( |
| JNIEnv* env, |
| const base::android::JavaParamRef<jbyteArray>& cbor_java) { |
| absl::optional<std::vector<uint8_t>> optional_cbor; |
| |
| if (cbor_java) { |
| std::vector<uint8_t> cbor; |
| base::android::JavaByteArrayToByteVector(env, cbor_java, &cbor); |
| optional_cbor = std::move(cbor); |
| } |
| |
| content::BrowserThread::GetTaskRunnerForThread(content::BrowserThread::UI) |
| ->PostTask(FROM_HERE, |
| base::BindOnce(&OnHavePlayServicesLinkingInformation, |
| std::move(optional_cbor))); |
| } |
| |
| static void JNI_PrivacySettingsFragment_RevokeAllLinkedDevices(JNIEnv* env) { |
| // Invalidates the current cloud messaging (GCM) token and creates a new one. |
| // This causes the tunnel server to reject connection attempts with a 410 |
| // (Gone) error. Since linking keys are derived from the root secret by using |
| // the GCM token, this also invalidates all existing linking keys. |
| webauthn::authenticator::GetRegistrationState() |
| ->linking_registration() |
| ->RotateContactID(); |
| } |