blob: 325c60bb076045ea6719bedfc86cb929f35818f3 [file] [log] [blame]
// Copyright 2018 The Chromium OS 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 "biod/cros_fp_device.h"
#include <fcntl.h>
#include <sys/ioctl.h>
#include <algorithm>
#include <base/strings/string_number_conversions.h>
#include <base/strings/stringprintf.h>
namespace {
// Upper bound of the host command packet transfer size.
constexpr int kMaxPacketSize = 544;
std::string FourCC(const uint32_t a) {
return base::StringPrintf(
"%c%c%c%c", static_cast<char>(a), static_cast<char>(a >> 8),
static_cast<char>(a >> 16), static_cast<char>(a >> 24));
}
} // namespace
namespace biod {
constexpr char CrosFpDevice::kCrosFpPath[];
CrosFpDevice::~CrosFpDevice() {
// Current session is gone, clean-up temporary state in the FP MCU.
if (cros_fd_.is_valid())
ResetContext();
}
std::unique_ptr<CrosFpDevice> CrosFpDevice::Open(const MkbpCallback& callback) {
std::unique_ptr<CrosFpDevice> dev = std::make_unique<CrosFpDevice>(callback);
if (!dev->Init())
return nullptr;
return dev;
}
bool CrosFpDevice::EcProtoInfo(ssize_t* max_read, ssize_t* max_write) {
/* read max request / response size from the MCU for protocol v3+ */
EcCommand<EmptyParam, struct ec_response_get_protocol_info> cmd(
EC_CMD_GET_PROTOCOL_INFO);
if (!cmd.Run(cros_fd_.get()))
return false;
*max_read =
cmd.Resp()->max_response_packet_size - sizeof(struct ec_host_response);
// TODO(vpalatin): workaround for b/78544921, can be removed if MCU is fixed.
*max_write =
cmd.Resp()->max_request_packet_size - sizeof(struct ec_host_request) - 4;
return true;
}
bool CrosFpDevice::EcDevInit() {
char version[80];
int ret = read(cros_fd_.get(), version, sizeof(version) - 1);
if (ret <= 0) {
int err = ret ? errno : 0;
LOG(ERROR) << "Failed to read cros_fp device: " << err;
return false;
}
version[ret] = '\0';
LOG(INFO) << "cros_fp device version: " << version;
char* s = strchr(version, '\n');
if (s)
*s = '\0';
if (strcmp(version, CROS_EC_DEV_VERSION)) {
LOG(ERROR) << "Invalid device version";
return false;
}
if (!EcProtoInfo(&max_read_size_, &max_write_size_)) {
LOG(ERROR) << "Fail to get cros_fp protocol info " << ret;
return false;
}
unsigned long mask = 1 << EC_MKBP_EVENT_FINGERPRINT; // NOLINT(runtime/int)
if (ioctl(cros_fd_.get(), CROS_EC_DEV_IOCEVENTMASK_V2, mask) < 0) {
LOG(ERROR) << "Fail to request fingerprint events";
return false;
}
return true;
}
void CrosFpDevice::OnFileCanReadWithoutBlocking(int fd) {
struct ec_response_get_next_event evt;
ssize_t sz = read(cros_fd_.get(), &evt, sizeof(evt));
// We are interested only in fingerprint events, discard the other ones.
if (evt.event_type != EC_MKBP_EVENT_FINGERPRINT ||
sz < sizeof(evt.event_type) + sizeof(evt.data.fp_events))
return;
// Properly aligned event value.
uint32_t events;
memcpy(&events, &evt.data.fp_events, sizeof(events));
mkbp_event_.Run(events);
}
bool CrosFpDevice::FpMode(uint32_t mode) {
EcCommand<struct ec_params_fp_mode, struct ec_response_fp_mode> cmd(
EC_CMD_FP_MODE, 0, {.mode = mode});
bool ret = cmd.Run(cros_fd_.get());
if (ret) {
return true;
}
// In some cases the EC Command might go through, but the AP suspends
// before the EC can ACK it. When the AP wakes up, it considers the
// EC command to have timed out. Since this seems to happen during mode
// setting, check the mode in case of a failure.
uint32_t cur_mode;
if (!GetFpMode(&cur_mode)) {
LOG(ERROR) << "Failed to get FP mode to verify mode was set in the MCU.";
return false;
}
if (cur_mode == mode) {
LOG(WARNING)
<< "EC Command to set mode failed, but mode was set successfully.";
return true;
} else {
LOG(ERROR) << "EC command to set FP mode: " << mode
<< " failed; current FP mode: " << cur_mode;
}
return false;
}
bool CrosFpDevice::GetFpMode(uint32_t* mode) {
EcCommand<struct ec_params_fp_mode, struct ec_response_fp_mode> cmd(
EC_CMD_FP_MODE, 0, {.mode = static_cast<uint32_t>(FP_MODE_DONT_CHANGE)});
if (!cmd.Run(cros_fd_.get())) {
LOG(ERROR) << "Failed to get FP mode from MCU.";
return false;
}
*mode = cmd.Resp()->mode;
return true;
}
bool CrosFpDevice::FpFrame(int index, std::vector<uint8_t>* frame) {
EcCommand<struct ec_params_fp_frame, uint8_t[kMaxPacketSize]> cmd(
EC_CMD_FP_FRAME);
uint32_t offset = index << FP_FRAME_INDEX_SHIFT;
uint8_t* payload = cmd.Resp()[0];
auto pos = frame->begin();
while (pos < frame->end()) {
uint32_t len = std::min(max_read_size_, frame->end() - pos);
cmd.SetReq({.offset = offset, .size = len});
cmd.SetRespSize(len);
int retries = 0;
const int max_retries = 50, delay_ms = 100;
while (!cmd.Run(cros_fd_.get())) {
if (!(offset & FP_FRAME_OFFSET_MASK)) {
// On the first request, the EC might still be rate-limiting. Retry in
// that case.
if (cmd.Result() == EC_RES_BUSY && retries < max_retries) {
retries++;
LOG(INFO) << "Retrying FP_FRAME, attempt " << retries;
base::PlatformThread::Sleep(
base::TimeDelta::FromMilliseconds(delay_ms));
continue;
}
}
LOG(ERROR) << "FP_FRAME command failed @ 0x" << std::hex << offset;
return false;
}
std::copy(payload, payload + len, pos);
offset += len;
pos += len;
}
return true;
}
bool CrosFpDevice::UpdateFpInfo() {
EcCommand<EmptyParam, struct ec_response_fp_info> cmd(EC_CMD_FP_INFO, 1);
if (!cmd.Run(cros_fd_.get())) {
LOG(ERROR) << "Failed to get FP information";
return false;
}
info_ = *cmd.Resp();
return true;
}
bool CrosFpDevice::GetFpStats(int* capture_ms,
int* matcher_ms,
int* overall_ms) {
EcCommand<EmptyParam, struct ec_response_fp_stats> cmd(EC_CMD_FP_STATS);
if (!cmd.Run(cros_fd_.get()))
return false;
uint8_t inval = cmd.Resp()->timestamps_invalid;
if (inval & (FPSTATS_CAPTURE_INV | FPSTATS_MATCHING_INV))
return false;
*capture_ms = cmd.Resp()->capture_time_us / 1000;
*matcher_ms = cmd.Resp()->matching_time_us / 1000;
*overall_ms = cmd.Resp()->overall_time_us / 1000;
return true;
}
// static
bool CrosFpDevice::WaitOnEcBoot(const base::ScopedFD& cros_fp_fd,
ec_current_image expected_image) {
int tries = 50;
ec_current_image image = EC_IMAGE_UNKNOWN;
while (tries) {
tries--;
// Check the EC has the right image.
EcCommand<EmptyParam, struct ec_response_get_version> cmd(
EC_CMD_GET_VERSION);
if (!cmd.Run(cros_fp_fd.get())) {
LOG(ERROR) << "Failed to retrieve cros_fp firmware version.";
base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(500));
continue;
}
image = static_cast<ec_current_image>(cmd.Resp()->current_image);
if (image == expected_image) {
LOG(INFO) << "EC image is " << (image == EC_IMAGE_RO ? "RO" : "RW")
<< ".";
return true;
}
base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(100));
}
LOG(ERROR) << "EC rebooted to incorrect image " << image;
return false;
}
bool CrosFpDevice::EcReboot(ec_current_image to_image) {
DCHECK(to_image == EC_IMAGE_RO || to_image == EC_IMAGE_RW);
EcCommand<EmptyParam, EmptyParam> cmd_reboot(EC_CMD_REBOOT);
// Don't expect a return code, cros_fp has rebooted.
cmd_reboot.Run(cros_fd_.get());
if (!WaitOnEcBoot(cros_fd_, EC_IMAGE_RO)) {
LOG(ERROR) << "EC did not come back up after reboot.";
return false;
}
if (to_image == EC_IMAGE_RO) {
// Tell the EC to remain in RO.
EcCommand<struct ec_params_rwsig_action, EmptyParam> cmd_rwsig(
EC_CMD_RWSIG_ACTION);
cmd_rwsig.SetReq({.action = RWSIG_ACTION_ABORT});
if (!cmd_rwsig.Run(cros_fd_.get())) {
LOG(ERROR) << "Failed to keep cros_fp in RO.";
return false;
}
}
// EC jumps to RW after 1 second. Wait enough time in case we want to reboot
// to RW. In case we wanted to remain in RO, wait anyway to ensure that the EC
// received the instructions.
base::PlatformThread::Sleep(base::TimeDelta::FromSeconds(3));
if (!WaitOnEcBoot(cros_fd_, to_image)) {
LOG(ERROR) << "EC did not load the right image.";
return false;
}
return true;
}
bool CrosFpDevice::AddEntropy(bool reset) {
// Create the secret.
EcCommand<struct ec_params_rollback_add_entropy, EmptyParam> cmd_add_entropy(
EC_CMD_ADD_ENTROPY);
if (reset) {
cmd_add_entropy.SetReq({.action = ADD_ENTROPY_RESET_ASYNC});
} else {
cmd_add_entropy.SetReq({.action = ADD_ENTROPY_ASYNC});
}
if (!cmd_add_entropy.Run(cros_fd_.get())) {
LOG(ERROR) << "Failed to send command to add entropy.";
return false;
}
int tries = 20;
while (tries) {
tries--;
base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(100));
cmd_add_entropy.SetReq({.action = ADD_ENTROPY_GET_RESULT});
// EC will return EC_RES_BUSY initially, ignore the return code.
cmd_add_entropy.Run(cros_fd_.get());
if (cmd_add_entropy.Result() == EC_RES_SUCCESS) {
LOG(INFO) << "Entropy has been successfully added.";
return true;
}
}
LOG(ERROR) << "Failed to check status of entropy command.";
return false;
}
bool CrosFpDevice::GetRollBackInfoId(int32_t* block_id) {
EcCommand<EmptyParam, struct ec_response_rollback_info> cmd_rb_info(
EC_CMD_ROLLBACK_INFO);
if (!cmd_rb_info.Run(cros_fd_.get())) {
return false;
}
*block_id = cmd_rb_info.Resp()->id;
return true;
}
bool CrosFpDevice::InitEntropy(bool reset) {
int32_t block_id;
if (!GetRollBackInfoId(&block_id)) {
LOG(ERROR) << "Failed to read block ID from FPMCU.";
return false;
}
if (!reset && block_id != 0) {
// Secret has been set.
LOG(INFO) << "Entropy source had been initialized previously.";
return true;
}
LOG(INFO) << "Entropy source has not been initialized yet.";
bool success = UpdateEntropy(reset);
if (!success) {
LOG(INFO) << "Entropy addition failed.";
return false;
}
LOG(INFO) << "Entropy has been successfully added.";
return true;
}
bool CrosFpDevice::Init() {
cros_fd_ = base::ScopedFD(open(kCrosFpPath, O_RDWR));
if (cros_fd_.get() < 0) {
LOG(ERROR) << "Failed to open " << kCrosFpPath;
return false;
}
if (!EcDevInit())
return false;
if (!InitEntropy(false)) {
return false;
}
// Clean MCU memory if anything is remaining from aborted sessions
ResetContext();
// Retrieve the sensor information / parameters.
if (!UpdateFpInfo())
return false;
LOG(INFO) << "CROS FP Sensor Info ";
LOG(INFO) << " Vendor ID : " << FourCC(info_.vendor_id);
LOG(INFO) << " Product ID : " << info_.product_id;
LOG(INFO) << " Model ID : 0x" << std::hex << info_.model_id;
LOG(INFO) << " Version : " << info_.version;
std::string error_flags;
if (info_.errors & FP_ERROR_NO_IRQ)
error_flags += "NO_IRQ ";
if (info_.errors & FP_ERROR_SPI_COMM)
error_flags += "SPI_COMM ";
if (info_.errors & FP_ERROR_BAD_HWID)
error_flags += "BAD_HWID ";
if (info_.errors & FP_ERROR_INIT_FAIL)
error_flags += "INIT_FAIL";
LOG(INFO) << " Errors : " << error_flags;
LOG(INFO) << "CROS FP Image Info ";
// Prints the pixel format in FOURCC format.
LOG(INFO) << " Pixel Format : " << FourCC(info_.pixel_format);
LOG(INFO) << " Image Data Size : " << info_.frame_size;
LOG(INFO) << " Image Dimensions : " << info_.width << "x" << info_.height
<< " " << info_.bpp << " bpp";
LOG(INFO) << "CROS FP Finger Template Info ";
LOG(INFO) << " Template data format : " << info_.template_version;
LOG(INFO) << " Template Data Size : " << info_.template_size;
LOG(INFO) << " Max number of fingers : " << info_.template_max;
if (!MessageLoopForIO::current()->WatchFileDescriptor(
cros_fd_.get(), true, MessageLoopForIO::WATCH_READ, fd_watcher_.get(),
this)) {
LOG(ERROR) << "Unable to watch MKBP events";
return false;
}
if (!input_device_.Init()) {
LOG(ERROR) << "Failed to create Uinput device";
return false;
}
return true;
}
bool CrosFpDevice::GetDirtyMap(std::bitset<32>* bitmap) {
// Retrieve the up-to-date dirty bitmap from the MCU.
if (!UpdateFpInfo())
return false;
*bitmap = std::bitset<32>(info_.template_dirty);
return true;
}
bool CrosFpDevice::GetTemplate(int index, VendorTemplate* out) {
if (index == kLastTemplate) {
// Get the count of valid templates and the dirty bitmap.
if (!UpdateFpInfo())
return false;
// Use the last template.
index = info_.template_valid - 1;
// Is the last one really a new created one ?
if (!(info_.template_dirty & (1 << index)))
return false;
}
out->resize(static_cast<size_t>(info_.template_size));
// In the EC_CMD_FP_FRAME host command, the templates are indexed starting
// from 1 (aka FP_FRAME_INDEX_TEMPLATE), as 0 (aka FP_FRAME_INDEX_RAW_IMAGE)
// is used for the finger image.
return FpFrame(index + FP_FRAME_INDEX_TEMPLATE, out);
}
bool CrosFpDevice::UploadTemplate(const VendorTemplate& tmpl) {
union cmd_with_data {
struct ec_params_fp_template req;
uint8_t _fullsize[kMaxPacketSize];
};
EcCommand<union cmd_with_data, EmptyParam> cmd(EC_CMD_FP_TEMPLATE);
struct ec_params_fp_template* req = &cmd.Req()->req;
size_t max_chunk =
max_write_size_ - offsetof(struct ec_params_fp_template, data);
auto pos = tmpl.begin();
while (pos < tmpl.end()) {
size_t remaining = tmpl.end() - pos;
uint32_t tlen = std::min(max_chunk, remaining);
req->offset = pos - tmpl.begin();
req->size = tlen | (remaining == tlen ? FP_TEMPLATE_COMMIT : 0);
std::copy(pos, pos + tlen, req->data);
cmd.SetReqSize(tlen + sizeof(struct ec_params_fp_template));
if (!cmd.Run(cros_fd_.get()) || cmd.Result() != EC_RES_SUCCESS) {
LOG(ERROR) << "FP_TEMPLATE command failed @ " << pos - tmpl.begin();
return false;
}
pos += tlen;
}
return true;
}
bool CrosFpDevice::SetContext(std::string user_hex) {
struct ec_params_fp_context ctxt = {};
if (!user_hex.empty()) {
std::vector<uint8_t> user_id;
if (base::HexStringToBytes(user_hex, &user_id))
memcpy(ctxt.userid, user_id.data(),
std::min(user_id.size(), sizeof(ctxt.userid)));
}
EcCommand<struct ec_params_fp_context, EmptyParam> cmd(EC_CMD_FP_CONTEXT, 0,
ctxt);
return cmd.Run(cros_fd_.get());
}
bool CrosFpDevice::ResetContext() {
return SetContext(std::string());
}
bool CrosFpDevice::UpdateEntropy(bool reset) {
// Stash the most recent block id.
int32_t block_id;
if (!GetRollBackInfoId(&block_id)) {
LOG(ERROR) << "Failed to block ID from FPMCU before entropy reset.";
return false;
}
// Reboot the EC to RO.
if (!EcReboot(EC_IMAGE_RO)) {
LOG(ERROR) << "Failed to reboot cros_fp to initialise entropy.";
return false;
}
// Initialize the secret.
if (!AddEntropy(reset)) {
LOG(ERROR) << "Failed to add entropy.";
return false;
}
// Entropy added, reboot cros_fp to RW.
if (!EcReboot(EC_IMAGE_RW)) {
LOG(ERROR) << "Failed to reboot cros_fp after initializing entropy.";
return false;
}
int32_t new_block_id;
if (!GetRollBackInfoId(&new_block_id)) {
LOG(ERROR) << "Failed to block ID from FPMCU after entropy reset.";
return false;
}
int32_t block_id_diff = 2;
if (!reset) {
block_id_diff = 1;
}
if (new_block_id != block_id + block_id_diff) {
LOG(ERROR) << "Entropy source has not been updated; old block_id: "
<< block_id << ", new block_id: " << new_block_id;
return false;
}
return true;
}
} // namespace biod