chromium/chromeos/ash/components/dbus/biod/fake_biod_client.cc

// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chromeos/ash/components/dbus/biod/fake_biod_client.h"

#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include <vector>

#include "base/containers/contains.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/logging.h"
#include "base/notreached.h"
#include "base/path_service.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/task/single_thread_task_runner.h"
#include "base/threading/thread_restrictions.h"
#include "base/values.h"
#include "dbus/object_path.h"
#include "third_party/cros_system_api/dbus/service_constants.h"

namespace ash {

namespace {

// Path of an enroll session. There should only be one enroll session at a
// given time.
const char kEnrollSessionObjectPath[] = "/EnrollSession";

// Header of the path of an record. A unique number will be appended when an
// record is created.
const char kRecordObjectPathPrefix[] = "/Record/";

// Path of an auth session. There should only be one auth sesion at a given
// time.
const char kAuthSessionObjectPath[] = "/AuthSession";

FakeBiodClient* g_instance = nullptr;

FakeBiodClient::FakeRecord ParseFakeRecordDict(
    const base::Value::Dict& fake_record_dict) {
  FakeBiodClient::FakeRecord res;
  for (const auto [key, value] : fake_record_dict) {
    if (key == "fingerprints") {
      for (const auto& fp_entry : value.GetList()) {
        res.fake_fingerprint.push_back(fp_entry.GetString());
      }
    } else if (key == "user_id") {
      res.user_id = value.GetString();
    } else if (key == "label") {
      res.label = value.GetString();
    } else {
      NOTREACHED_IN_MIGRATION();
    }
  }
  return res;
}

base::Value::Dict FakeRecordsToValue(const FakeBiodClient::RecordMap& records) {
  base::Value::Dict res;
  for (const auto& entry : records) {
    const std::string& entry_key = entry.first.value();
    const FakeBiodClient::FakeRecord& entry_fake_record = entry.second;
    base::Value::List fake_images;
    for (const std::string& fake_image : entry_fake_record.fake_fingerprint) {
      fake_images.Append(fake_image);
    }
    base::Value::Dict cur_record;
    cur_record.Set("fingerprints", std::move(fake_images));
    cur_record.Set("user_id", entry_fake_record.user_id);
    cur_record.Set("label", entry_fake_record.label);
    res.Set(entry_key, std::move(cur_record));
  }
  return res;
}

FakeBiodClient::RecordMap ValueToFakeRecords(const base::Value& records_val) {
  FakeBiodClient::RecordMap records;
  const base::Value::Dict& fake_biod_db_dict = records_val.GetDict();
  for (const auto fake_biod_db_entry : fake_biod_db_dict) {
    const base::Value::Dict& fake_record_dict =
        fake_biod_db_entry.second.GetDict();
    records.try_emplace(dbus::ObjectPath(fake_biod_db_entry.first),
                        ParseFakeRecordDict(fake_record_dict));
  }
  return records;
}

int GetNextRecordId(const FakeBiodClient::RecordMap& records) {
  int next_record_unique_id = 1;
  for (const auto& [key, _] : records) {
    std::vector<std::string_view> splitted_str = base::SplitStringPiece(
        key.value(), "/", base::WhitespaceHandling::TRIM_WHITESPACE,
        base::SplitResult::SPLIT_WANT_NONEMPTY);
    CHECK_EQ(splitted_str.size(), static_cast<size_t>(2));
    int record_id = 0;
    CHECK(base::StringToInt(splitted_str[1], &record_id));
    next_record_unique_id = std::max(next_record_unique_id, record_id + 1);
  }
  return next_record_unique_id;
}

}  // namespace

FakeBiodClient::FakeRecord::FakeRecord() = default;
FakeBiodClient::FakeRecord::FakeRecord(const FakeRecord&) = default;
FakeBiodClient::FakeRecord::~FakeRecord() = default;

void FakeBiodClient::FakeRecord::Clear() {
  user_id.clear();
  label.clear();
  fake_fingerprint.clear();
}

FakeBiodClient::FakeBiodClient() {
  CHECK(!g_instance);
  g_instance = this;
}

FakeBiodClient::~FakeBiodClient() {
  CHECK_EQ(this, g_instance);
  g_instance = nullptr;
}

// static
FakeBiodClient* FakeBiodClient::Get() {
  return g_instance;
}

void FakeBiodClient::SendRestarted() {
  current_session_ = FingerprintSession::NONE;

  for (auto& observer : observers_)
    observer.BiodServiceRestarted();
}

void FakeBiodClient::SendStatusChanged(biod::BiometricsManagerStatus status) {
  current_session_ = FingerprintSession::NONE;

  for (auto& observer : observers_) {
    observer.BiodServiceStatusChanged(status);
  }
}

void FakeBiodClient::SendEnrollScanDone(const std::string& fingerprint,
                                        biod::ScanResult type_result,
                                        bool is_complete,
                                        int percent_complete) {
  // Enroll scan signals do nothing if an enroll session is not happening.
  if (current_session_ != FingerprintSession::ENROLL)
    return;

  // The fake fingerprint gets appended to the current fake fingerprints.
  current_record_.fake_fingerprint.push_back(fingerprint);

  // If the enroll is complete, save the record and exit enroll mode.
  if (is_complete) {
    records_[current_record_path_] = std::move(current_record_);
    SaveRecords();
    current_record_path_ = dbus::ObjectPath();
    current_record_.Clear();
    current_session_ = FingerprintSession::NONE;
  }

  for (auto& observer : observers_)
    observer.BiodEnrollScanDoneReceived(type_result, is_complete,
                                        percent_complete);
}

void FakeBiodClient::SendAuthScanDone(const std::string& fingerprint,
                                      const biod::FingerprintMessage& msg) {
  // Auth scan signals do nothing if an auth session is not happening.
  if (current_session_ != FingerprintSession::AUTH)
    return;

  AuthScanMatches matches;
  if (msg.msg_case() == biod::FingerprintMessage::MsgCase::kScanResult &&
      msg.scan_result() == biod::ScanResult::SCAN_RESULT_SUCCESS) {
    // Iterate through all the records to check if fingerprint is a match and
    // populate |matches| accordingly. This searches through all the records and
    // then each record's fake fingerprint, but neither of these should ever
    // have more than five entries.
    for (const auto& entry : records_) {
      const FakeRecord& record = entry.second;
      if (base::Contains(record.fake_fingerprint, fingerprint)) {
        const std::string& user_id = record.user_id;
        matches[user_id].push_back(entry.first);
      }
    }
  }

  for (auto& observer : observers_)
    observer.BiodAuthScanDoneReceived(msg, matches);
}

void FakeBiodClient::SendSessionFailed() {
  if (current_session_ == FingerprintSession::NONE)
    return;

  for (auto& observer : observers_)
    observer.BiodSessionFailedReceived();
}

void FakeBiodClient::Reset() {
  records_.clear();
  current_record_.Clear();
  current_record_path_ = dbus::ObjectPath();
  current_session_ = FingerprintSession::NONE;
}

void FakeBiodClient::AddObserver(Observer* observer) {
  observers_.AddObserver(observer);
}

void FakeBiodClient::RemoveObserver(Observer* observer) {
  observers_.RemoveObserver(observer);
}

bool FakeBiodClient::HasObserver(const Observer* observer) const {
  return observers_.HasObserver(observer);
}

void FakeBiodClient::StartEnrollSession(const std::string& user_id,
                                        const std::string& label,
                                        chromeos::ObjectPathCallback callback) {
  CHECK_EQ(current_session_, FingerprintSession::NONE);
  current_enroll_percentage_ = 0;

  // Create the enrollment with |user_id|, |label| and a empty fake fingerprint.
  current_record_path_ = dbus::ObjectPath(
      kRecordObjectPathPrefix + base::NumberToString(next_record_unique_id_++));
  current_record_.user_id = user_id;
  current_record_.label = label;
  current_session_ = FingerprintSession::ENROLL;

  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE, base::BindOnce(std::move(callback),
                                dbus::ObjectPath(kEnrollSessionObjectPath)));
}

void FakeBiodClient::SetFakeUserDataDir(const base::FilePath& path) {
  fake_biod_db_filepath_ = path.Append("fake_biod");
  LoadRecords();
}

void FakeBiodClient::GetRecordsForUser(const std::string& user_id,
                                       UserRecordsCallback callback) {
  std::vector<dbus::ObjectPath> records_object_paths;
  for (const auto& record : records_) {
    if (record.second.user_id == user_id) {
      records_object_paths.push_back(record.first);
    }
  }

  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE,
      base::BindOnce(std::move(callback), records_object_paths, true));
}

void FakeBiodClient::DestroyAllRecords(
    chromeos::VoidDBusMethodCallback callback) {
  records_.clear();
  SaveRecords();

  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE, base::BindOnce(std::move(callback), true));
}

void FakeBiodClient::StartAuthSession(chromeos::ObjectPathCallback callback) {
  CHECK_EQ(current_session_, FingerprintSession::NONE);

  current_session_ = FingerprintSession::AUTH;
  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE, base::BindOnce(std::move(callback),
                                dbus::ObjectPath(kAuthSessionObjectPath)));
}

void FakeBiodClient::RequestType(BiometricTypeCallback callback) {
  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE,
      base::BindOnce(std::move(callback), biod::BIOMETRIC_TYPE_FINGERPRINT));
}

void FakeBiodClient::CancelEnrollSession(
    chromeos::VoidDBusMethodCallback callback) {
  CHECK_EQ(current_session_, FingerprintSession::ENROLL);
  current_enroll_percentage_ = 0;

  // Clean up the in progress enrollment.
  current_record_.Clear();
  current_record_path_ = dbus::ObjectPath();
  current_session_ = FingerprintSession::NONE;

  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE, base::BindOnce(std::move(callback), true));
}

void FakeBiodClient::EndAuthSession(chromeos::VoidDBusMethodCallback callback) {
  current_session_ = FingerprintSession::NONE;
  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE, base::BindOnce(std::move(callback), true));
}

void FakeBiodClient::SetRecordLabel(const dbus::ObjectPath& record_path,
                                    const std::string& label,
                                    chromeos::VoidDBusMethodCallback callback) {
  auto it = records_.find(record_path);
  if (it != records_.end()) {
    it->second.label = label;
  }
  SaveRecords();
  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE, base::BindOnce(std::move(callback), true));
}

void FakeBiodClient::RemoveRecord(const dbus::ObjectPath& record_path,
                                  chromeos::VoidDBusMethodCallback callback) {
  records_.erase(record_path);
  SaveRecords();

  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE, base::BindOnce(std::move(callback), true));
}

void FakeBiodClient::RequestRecordLabel(const dbus::ObjectPath& record_path,
                                        LabelCallback callback) {
  std::string record_label;
  auto it = records_.find(record_path);
  if (it != records_.end()) {
    record_label = it->second.label;
  }

  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE, base::BindOnce(std::move(callback), record_label));
}

void FakeBiodClient::TouchFingerprintSensor(int finger_id) {
  CHECK(finger_id > 0 && finger_id < 4);
  switch (current_session_) {
    case FingerprintSession::NONE:
      break;
    case FingerprintSession::AUTH: {
      biod::FingerprintMessage auth_result;
      auth_result.set_scan_result(biod::ScanResult::SCAN_RESULT_SUCCESS);
      SendAuthScanDone(base::NumberToString(finger_id), auth_result);
      break;
    }
    case FingerprintSession::ENROLL:
      current_enroll_percentage_ += 100 / finger_id;
      current_enroll_percentage_ = std::min(100, current_enroll_percentage_);
      SendEnrollScanDone(base::NumberToString(finger_id),
                         biod::ScanResult::SCAN_RESULT_SUCCESS,
                         current_enroll_percentage_ == 100,
                         current_enroll_percentage_);
      break;
  }
}

void FakeBiodClient::SaveRecords() const {
  base::ScopedAllowBlockingForTesting allow_io;
  if (records_.empty()) {
    base::DeleteFile(fake_biod_db_filepath_);
    return;
  }
  const base::Value::Dict& record_dict = FakeRecordsToValue(records_);
  if (auto json_string = base::WriteJson(record_dict)) {
    if (base::WriteFile(fake_biod_db_filepath_, json_string.value())) {
      return;
    }
  }
  LOG(ERROR) << "FakeBiod SaveRecords failed.";
}

void FakeBiodClient::LoadRecords() {
  CHECK(records_.empty());
  base::ScopedAllowBlockingForTesting allow_io;
  if (!base::PathExists(fake_biod_db_filepath_)) {
    return;
  }
  std::string content;
  if (!base::ReadFileToString(fake_biod_db_filepath_, &content)) {
    LOG(ERROR) << "FakeBiod failed to read the file: "
               << fake_biod_db_filepath_;
    return;
  }
  std::optional<base::Value> records_json = base::JSONReader::Read(content);
  if (!records_json.has_value()) {
    LOG(ERROR) << "FakeBiod parse failed.";
    return;
  }

  records_ = ValueToFakeRecords(records_json.value());
  next_record_unique_id_ = GetNextRecordId(records_);
}

}  // namespace ash