blob: 94572f61fe4e3fb1e56c66715c65ff6e438839c0 [file] [log] [blame]
// Copyright 2016 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/fpc_biometrics_manager.h"
#include <algorithm>
#include <errno.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <base/base64.h>
#include <base/bind.h>
#include <base/logging.h>
#include <base/strings/string_piece.h>
#include <base/threading/thread_task_runner_handle.h>
#include "biod/fpc/fp_sensor.h"
namespace biod {
class FpcBiometricsManager::SensorLibrary {
public:
~SensorLibrary();
static std::unique_ptr<SensorLibrary> Open(
const std::shared_ptr<BioLibrary>& bio_lib, int fd);
BioEnrollment BeginEnrollment();
std::tuple<int, BioImage> AcquireImage();
bool WaitFingerUp();
bool Cancel();
private:
using fp_sensor_open_fp = int (*)(int fd);
using fp_sensor_close_fp = int (*)(void);
using fp_sensor_get_model_fp = int (*)(uint32_t* vendor_id,
uint32_t* product_id,
uint32_t* model_id,
uint32_t* version);
using fp_sensor_get_pixel_format_fp = int (*)(uint32_t* pixel_format);
using fp_sensor_get_image_data_size_fp = ssize_t (*)(void);
using fp_sensor_get_image_dimensions_fp = int (*)(uint32_t* width,
uint32_t* height);
using fp_sensor_acquire_image_fp = int (*)(uint8_t* image_data, size_t size);
using fp_sensor_wait_finger_up_fp = int (*)(void);
using fp_sensor_cancel_fp = int (*)(void);
explicit SensorLibrary(const std::shared_ptr<BioLibrary>& bio_lib)
: bio_lib_(bio_lib) {}
bool Init(int fd);
fp_sensor_open_fp open_;
fp_sensor_close_fp close_;
fp_sensor_get_model_fp get_model_;
fp_sensor_get_pixel_format_fp get_pixel_format_;
fp_sensor_get_image_data_size_fp get_image_data_size_;
fp_sensor_get_image_dimensions_fp get_image_dimensions_;
fp_sensor_acquire_image_fp acquire_image_;
fp_sensor_wait_finger_up_fp wait_finger_up_;
fp_sensor_cancel_fp cancel_;
std::shared_ptr<BioLibrary> bio_lib_;
bool needs_close_ = false;
size_t image_data_size_ = 0;
BioSensor bio_sensor_;
DISALLOW_COPY_AND_ASSIGN(SensorLibrary);
};
FpcBiometricsManager::SensorLibrary::~SensorLibrary() {
if (needs_close_)
close_();
}
std::unique_ptr<FpcBiometricsManager::SensorLibrary>
FpcBiometricsManager::SensorLibrary::Open(
const std::shared_ptr<BioLibrary>& bio_lib, int fd) {
std::unique_ptr<SensorLibrary> lib(new SensorLibrary(bio_lib));
if (!lib->Init(fd))
return nullptr;
return lib;
}
BioEnrollment FpcBiometricsManager::SensorLibrary::BeginEnrollment() {
return bio_sensor_.BeginEnrollment();
}
std::tuple<int, BioImage> FpcBiometricsManager::SensorLibrary::AcquireImage() {
std::vector<uint8_t> image_data(image_data_size_);
int acquire_result = acquire_image_(image_data.data(), image_data.size());
if (acquire_result)
return std::tuple<int, BioImage>(acquire_result, BioImage());
BioImage image = bio_sensor_.CreateImage();
if (!image || !image.SetData(&image_data)) {
LOG(ERROR) << "Failed to construct BioImage for image acquired.";
return std::tuple<int, BioImage>(-ENOMEM, BioImage());
}
return std::tuple<int, BioImage>(0 /* success */, std::move(image));
}
bool FpcBiometricsManager::SensorLibrary::WaitFingerUp() {
int ret = wait_finger_up_();
if (ret)
LOG(ERROR) << "Failed to wait for finger up: " << ret;
return ret == 0;
}
bool FpcBiometricsManager::SensorLibrary::Cancel() {
int ret = cancel_();
if (ret)
LOG(ERROR) << "Failed to cancel FPC sensor operation: " << ret;
return ret == 0;
}
bool FpcBiometricsManager::SensorLibrary::Init(int fd) {
#define SENSOR_SYM(x) \
do { \
x##_ = bio_lib_->GetFunction<fp_sensor_##x##_fp>("fp_sensor_" #x); \
if (!x##_) { \
LOG(ERROR) << #x " is missing from library"; \
return false; \
} \
} while (0)
// Here we use the very same shared object loaded by BioLibrary to pull out
// some private functions that interface with the FPC sensor.
SENSOR_SYM(open);
SENSOR_SYM(close);
SENSOR_SYM(get_model);
SENSOR_SYM(get_pixel_format);
SENSOR_SYM(get_image_data_size);
SENSOR_SYM(get_image_dimensions);
SENSOR_SYM(acquire_image);
SENSOR_SYM(wait_finger_up);
SENSOR_SYM(cancel);
#undef SENSOR_SYM
int ret = open_(fd);
if (ret) {
LOG(ERROR) << "Failed to open sensor library: " << ret;
return false;
}
needs_close_ = true;
BioSensor::Model model;
ret = get_model_(
&model.vendor_id, &model.product_id, &model.model_id, &model.version);
if (ret) {
LOG(ERROR) << "Failed to get sensor model: " << ret;
return false;
}
uint32_t pixel_format;
ret = get_pixel_format_(&pixel_format);
if (ret) {
LOG(ERROR) << "Failed to get sensor pixel format: " << ret;
return false;
}
ssize_t image_data_size = get_image_data_size_();
if (image_data_size <= 0) {
LOG(ERROR) << "Failed to get sensor image data size: " << image_data_size;
return false;
}
image_data_size_ = image_data_size;
uint32_t width, height;
ret = get_image_dimensions_(&width, &height);
if (ret) {
LOG(ERROR) << "Failed to get sensor image dimensions: " << ret;
return false;
}
LOG(INFO) << "FPC Sensor Info ";
LOG(INFO) << " Vendor ID : " << model.vendor_id;
LOG(INFO) << " Product ID : " << model.product_id;
LOG(INFO) << " Model ID : " << model.model_id;
LOG(INFO) << " Version : " << model.version;
LOG(INFO) << "FPC Image Info ";
// Prints the pixel format in FOURCC format.
const uint32_t a = pixel_format;
LOG(INFO) << " Pixel Format : " << static_cast<char>(a)
<< static_cast<char>(a >> 8) << static_cast<char>(a >> 16)
<< static_cast<char>(a >> 24);
LOG(INFO) << " Image Data Size : " << image_data_size;
LOG(INFO) << " Image Dimensions : " << width << "x" << height;
bio_sensor_ = bio_lib_->CreateSensor();
if (!bio_sensor_)
return false;
if (!bio_sensor_.SetModel(model))
return false;
if (!bio_sensor_.SetFormat(pixel_format))
return false;
if (!bio_sensor_.SetSize(width, height))
return false;
return true;
}
const std::string& FpcBiometricsManager::Record::GetId() const {
return id_;
}
const std::string& FpcBiometricsManager::Record::GetUserId() const {
CHECK(WithInternal([this](RecordIterator i) {
this->local_user_id_ = i->second.user_id;
})) << ": Attempted to get user ID for invalid BiometricsManager Record";
return local_user_id_;
}
const std::string& FpcBiometricsManager::Record::GetLabel() const {
CHECK(WithInternal([this](RecordIterator i) {
this->local_label_ = i->second.label;
})) << ": Attempted to get label for invalid BiometricsManager Record";
return local_label_;
}
bool FpcBiometricsManager::Record::SetLabel(std::string label) {
std::string old_label;
std::vector<uint8_t> serialized_tmpl;
CHECK(WithInternal([&](RecordIterator i) {
old_label = i->second.label;
i->second.label = std::move(label);
(i->second.tmpl).Serialize(&serialized_tmpl);
})) << ": Attempted to reset label for invalid BiometricsManager Record";
if (!biometrics_manager_->WriteRecord(
*this, serialized_tmpl.data(), serialized_tmpl.size())) {
CHECK(WithInternal(
[&](RecordIterator i) { i->second.label = std::move(old_label); }));
return false;
}
return true;
}
bool FpcBiometricsManager::Record::Remove() {
if (!biometrics_manager_)
return false;
if (!biometrics_manager_->biod_storage_.DeleteRecord(GetUserId(), GetId())) {
return false;
}
return WithInternal(
[this](RecordIterator i) { biometrics_manager_->records_.erase(i); });
}
int FpcBiometricsManager::g_sensor_fd = -1;
std::unique_ptr<BiometricsManager> FpcBiometricsManager::Create() {
std::unique_ptr<FpcBiometricsManager> biometrics_manager(
new FpcBiometricsManager);
if (!biometrics_manager->Init())
return nullptr;
return std::unique_ptr<BiometricsManager>(std::move(biometrics_manager));
}
BiometricType FpcBiometricsManager::GetType() {
return BIOMETRIC_TYPE_FINGERPRINT;
}
BiometricsManager::EnrollSession FpcBiometricsManager::StartEnrollSession(
std::string user_id, std::string label) {
if (running_task_)
return BiometricsManager::EnrollSession();
std::shared_ptr<BioTemplate> tmpl = std::make_shared<BioTemplate>();
kill_task_ = false;
bool task_will_run = sensor_thread_.task_runner()->PostTaskAndReply(
FROM_HERE,
base::Bind(&FpcBiometricsManager::DoEnrollSessionTask,
base::Unretained(this),
base::ThreadTaskRunnerHandle::Get(),
tmpl),
base::Bind(&FpcBiometricsManager::OnEnrollSessionComplete,
weak_factory_.GetWeakPtr(),
std::move(user_id),
std::move(label),
tmpl));
if (!task_will_run) {
LOG(ERROR) << "Failed to schedule EnrollSession task";
return BiometricsManager::EnrollSession();
}
// Note that the On*Complete function sets running_task_ to false on this
// thread's message loop, so setting it here does not result in a race
// condition.
running_task_ = true;
return BiometricsManager::EnrollSession(session_weak_factory_.GetWeakPtr());
}
BiometricsManager::AuthSession FpcBiometricsManager::StartAuthSession() {
if (running_task_)
return BiometricsManager::AuthSession();
std::shared_ptr<std::unordered_set<std::string>> updated_record_ids =
std::make_shared<std::unordered_set<std::string>>();
kill_task_ = false;
bool task_will_run = sensor_thread_.task_runner()->PostTaskAndReply(
FROM_HERE,
base::Bind(&FpcBiometricsManager::DoAuthSessionTask,
base::Unretained(this),
base::ThreadTaskRunnerHandle::Get(),
updated_record_ids),
base::Bind(&FpcBiometricsManager::OnAuthSessionComplete,
weak_factory_.GetWeakPtr(),
updated_record_ids));
if (!task_will_run) {
LOG(ERROR) << "Failed to schedule AuthSession task";
return BiometricsManager::AuthSession();
}
// Note that the On*Complete function sets running_task_ to false on this
// thread's message loop, so setting it here does not result in a race
// condition.
running_task_ = true;
return BiometricsManager::AuthSession(session_weak_factory_.GetWeakPtr());
}
std::vector<std::unique_ptr<BiometricsManager::Record>>
FpcBiometricsManager::GetRecords() {
base::AutoLock guard(records_lock_);
std::vector<std::unique_ptr<BiometricsManager::Record>> records(
records_.size());
std::transform(records_.begin(),
records_.end(),
records.begin(),
[this](decltype(records_)::value_type& record) {
return std::unique_ptr<BiometricsManager::Record>(
new Record(weak_factory_.GetWeakPtr(), record.first));
});
return records;
}
bool FpcBiometricsManager::DestroyAllRecords() {
base::AutoLock guard(records_lock_);
// Enumerate through records_ and delete each record.
bool delete_all_records = true;
for (auto& record_pair : records_) {
std::string record_id(record_pair.first);
std::string user_id(record_pair.second.user_id);
delete_all_records &= biod_storage_.DeleteRecord(user_id, record_id);
}
records_.clear();
return delete_all_records;
}
void FpcBiometricsManager::RemoveRecordsFromMemory() {
base::AutoLock guard(records_lock_);
records_.clear();
}
bool FpcBiometricsManager::ReadRecords(
const std::unordered_set<std::string>& user_ids) {
return biod_storage_.ReadRecords(user_ids);
}
void FpcBiometricsManager::SetEnrollScanDoneHandler(
const BiometricsManager::EnrollScanDoneCallback& on_enroll_scan_done) {
on_enroll_scan_done_ = on_enroll_scan_done;
}
void FpcBiometricsManager::SetAuthScanDoneHandler(
const BiometricsManager::AuthScanDoneCallback& on_auth_scan_done) {
on_auth_scan_done_ = on_auth_scan_done;
}
void FpcBiometricsManager::SetSessionFailedHandler(
const BiometricsManager::SessionFailedCallback& on_session_failed) {
on_session_failed_ = on_session_failed;
}
void FpcBiometricsManager::EndEnrollSession() {
KillSensorTask();
}
void FpcBiometricsManager::EndAuthSession() {
KillSensorTask();
}
void FpcBiometricsManager::KillSensorTask() {
{
base::AutoLock guard(kill_task_lock_);
kill_task_ = true;
}
sensor_lib_->Cancel();
}
FpcBiometricsManager::FpcBiometricsManager()
: sensor_thread_("fpc_sensor"),
session_weak_factory_(this),
weak_factory_(this),
biod_storage_(kFpcBiometricsManagerName,
base::Bind(&FpcBiometricsManager::LoadRecord,
base::Unretained(this))) {}
FpcBiometricsManager::~FpcBiometricsManager() {}
bool FpcBiometricsManager::Init() {
const char kFpcSensorPath[] = "/dev/fpc_sensor0";
sensor_fd_ = base::ScopedFD(open(kFpcSensorPath, O_RDWR));
g_sensor_fd = sensor_fd_.get();
if (sensor_fd_.get() < 0) {
LOG(ERROR) << "Failed to open " << kFpcSensorPath;
return false;
}
const char kFpcLibName[] = "/opt/fpc/lib/libfpsensor.so";
bio_lib_ = BioLibrary::Load(base::FilePath(kFpcLibName));
if (!bio_lib_)
return false;
sensor_lib_ = SensorLibrary::Open(bio_lib_, sensor_fd_.get());
if (!sensor_lib_)
return false;
if (!sensor_thread_.Start()) {
LOG(ERROR) << "Failed to start sensor thread";
return false;
}
return true;
}
void FpcBiometricsManager::OnEnrollScanDone(
ScanResult result, const BiometricsManager::EnrollStatus& enroll_status) {
if (!on_enroll_scan_done_.is_null())
on_enroll_scan_done_.Run(result, enroll_status);
}
void FpcBiometricsManager::OnAuthScanDone(
ScanResult result, const BiometricsManager::AttemptMatches& matches) {
if (!on_auth_scan_done_.is_null())
on_auth_scan_done_.Run(result, matches);
}
void FpcBiometricsManager::OnSessionFailed() {
if (!on_session_failed_.is_null())
on_session_failed_.Run();
}
FpcBiometricsManager::ScanData FpcBiometricsManager::ScanImage() {
DCHECK(sensor_thread_.task_runner()->RunsTasksOnCurrentThread());
bool success = sensor_lib_->WaitFingerUp();
{
base::AutoLock guard(kill_task_lock_);
if (kill_task_)
return ScanData(ScanData::Killed());
}
if (!success)
return ScanData();
int acquire_result, attempts = 0;
BioImage image;
// If the finger is positioned slightly off the sensor, retry a few times
// before failing. Typically the user has put their finger down and is now
// moving their finger to the correct position on the sensor. Instead of
// immediately failing, retry until we get a good image.
// Retry 20 times, which takes ~5s on Eve, before giving up and sending an
// error back to the user. Assume ~1s for user noticing that no matching
// happened, some time to move the finger on the sensor to allow a full
// capture and another 1s for the second matching attempt. 5s gives a bit of
// margin to avoid interrupting the user while they're moving the finger on
// the sensor.
const int kMaxPartialAttempts = 20;
do {
std::tie(acquire_result, image) = sensor_lib_->AcquireImage();
{
base::AutoLock guard(kill_task_lock_);
if (kill_task_)
return ScanData(ScanData::Killed());
}
++attempts;
} while (acquire_result == FP_SENSOR_LOW_SENSOR_COVERAGE &&
attempts < kMaxPartialAttempts);
switch (acquire_result) {
case 0:
break;
case FP_SENSOR_TOO_FAST:
return ScanData(ScanResult::SCAN_RESULT_TOO_FAST);
case FP_SENSOR_LOW_IMAGE_QUALITY:
return ScanData(ScanResult::SCAN_RESULT_INSUFFICIENT);
case FP_SENSOR_LOW_SENSOR_COVERAGE:
return ScanData(ScanResult::SCAN_RESULT_PARTIAL);
default:
LOG(ERROR) << "Unexpected result from acquiring image: "
<< acquire_result;
return ScanData();
}
return ScanData(std::move(image));
}
void FpcBiometricsManager::DoEnrollSessionTask(
const TaskRunnerRef& task_runner,
const std::shared_ptr<BioTemplate>& tmpl) {
DCHECK(sensor_thread_.task_runner()->RunsTasksOnCurrentThread());
if (kill_task_)
return;
BioEnrollment enrollment = sensor_lib_->BeginEnrollment();
if (!enrollment)
return;
for (;;) {
ScanData scan = ScanImage();
// The previous function can return early if this task was killed, or there
// was an unrecoverable hardware failure.
if (scan.killed || !scan.success)
return;
ScanResult scan_result = scan.result;
if (scan) {
int add_result = enrollment.AddImage(scan.image);
switch (add_result) {
case BIO_ENROLLMENT_OK:
break;
case BIO_ENROLLMENT_IMMOBILE:
scan_result = ScanResult::SCAN_RESULT_IMMOBILE;
break;
case BIO_ENROLLMENT_LOW_COVERAGE:
scan_result = ScanResult::SCAN_RESULT_PARTIAL;
break;
case BIO_ENROLLMENT_LOW_QUALITY:
scan_result = ScanResult::SCAN_RESULT_INSUFFICIENT;
break;
default:
LOG(ERROR) << "Unexpected result from add image: " << add_result;
return;
}
}
int complete_result = enrollment.IsComplete();
if (complete_result < 0) {
LOG(ERROR) << "Failed to get enrollment completion: " << complete_result;
return;
} else if (complete_result == 1) {
*tmpl = enrollment.Finish();
return;
} else {
BiometricsManager::EnrollStatus enroll_status = {
false, enrollment.GetPercentComplete()};
// Notice we will only ever post the on_enroll_scan_done task on an
// incomplete enrollment. The complete on_enroll_scan_done task is only
// posted after the enrollment is added to the enrollments(records) map,
// which is done only on the main thread's message loop.
bool task_will_run = task_runner->PostTask(
FROM_HERE,
base::Bind(&FpcBiometricsManager::OnEnrollScanDone,
base::Unretained(this),
scan_result,
enroll_status));
if (!task_will_run) {
LOG(ERROR) << "Failed to schedule EnrollScanDone callback";
return;
}
}
}
}
void FpcBiometricsManager::OnEnrollSessionComplete(
std::string user_id,
std::string label,
const std::shared_ptr<BioTemplate>& tmpl) {
OnTaskComplete();
if (kill_task_)
return;
// Remember tmpl stores a shared pointer to a /pointer/ which contains the
// actual result.
if (!*tmpl.get()) {
OnSessionFailed();
return;
}
std::vector<uint8_t> serialized_tmpl;
if (!tmpl->Serialize(&serialized_tmpl)) {
OnSessionFailed();
return;
}
std::string record_id(biod_storage_.GenerateNewRecordId());
{
base::AutoLock guard(records_lock_);
records_.emplace(
record_id,
InternalRecord{
std::move(user_id), std::move(label), std::move(*tmpl.get())});
}
Record current_record(weak_factory_.GetWeakPtr(), record_id);
if (!WriteRecord(
current_record, serialized_tmpl.data(), serialized_tmpl.size())) {
{
base::AutoLock guard(records_lock_);
records_.erase(record_id);
}
OnSessionFailed();
return;
}
BiometricsManager::EnrollStatus enroll_status = {true, 100};
OnEnrollScanDone(ScanResult::SCAN_RESULT_SUCCESS, enroll_status);
}
void FpcBiometricsManager::DoAuthSessionTask(
const TaskRunnerRef& task_runner,
std::shared_ptr<std::unordered_set<std::string>> updated_record_ids) {
DCHECK(sensor_thread_.task_runner()->RunsTasksOnCurrentThread());
if (kill_task_)
return;
BiometricsManager::AttemptMatches matches;
for (;;) {
ScanData scan = ScanImage();
// The previous function can return early if this task was killed, or there
// was an unrecoverable hardware failure.
if (scan.killed || !scan.success)
break;
ScanResult result = scan.result;
if (result == ScanResult::SCAN_RESULT_SUCCESS) {
matches.clear();
base::AutoLock guard(records_lock_);
for (auto& kv : records_) {
InternalRecord& record = kv.second;
int match_result = record.tmpl.MatchImage(scan.image);
switch (match_result) {
case BIO_TEMPLATE_NO_MATCH:
break;
case BIO_TEMPLATE_MATCH_UPDATED: // record.tmpl got updated
updated_record_ids->insert(kv.first);
case BIO_TEMPLATE_MATCH: {
auto emplace_result =
matches.emplace(record.user_id, std::vector<std::string>());
emplace_result.first->second.emplace_back(kv.first);
break;
}
case BIO_TEMPLATE_LOW_QUALITY:
result = ScanResult::SCAN_RESULT_INSUFFICIENT;
break;
case BIO_TEMPLATE_LOW_COVERAGE:
result = ScanResult::SCAN_RESULT_PARTIAL;
break;
default:
LOG(ERROR) << "Unexpected result from matching templates: "
<< match_result;
return;
}
}
}
// Assuming there was at least one match, we don't want to bother the user
// with error messages.
if (!matches.empty())
result = ScanResult::SCAN_RESULT_SUCCESS;
bool task_will_run =
task_runner->PostTask(FROM_HERE,
base::Bind(&FpcBiometricsManager::OnAuthScanDone,
base::Unretained(this),
result,
std::move(matches)));
if (!task_will_run) {
LOG(ERROR) << "Failed to schedule AuthScanDone callback";
return;
}
}
}
void FpcBiometricsManager::OnAuthSessionComplete(
std::shared_ptr<std::unordered_set<std::string>> updated_record_ids) {
OnTaskComplete();
// AuthSession never ends except on error or being killed. If no kill
// signal was given, we can assume failure.
if (!kill_task_)
OnSessionFailed();
for (const std::string& record_id : *updated_record_ids) {
InternalRecord& record = records_[record_id];
std::vector<uint8_t> serialized_tmpl;
if (!record.tmpl.Serialize(&serialized_tmpl)) {
LOG(ERROR) << "Cannot update record " << record_id
<< " in storage during AuthSession because template "
"serialization failed.";
continue;
}
Record current_record(weak_factory_.GetWeakPtr(), record_id);
if (!WriteRecord(
current_record, serialized_tmpl.data(), serialized_tmpl.size())) {
LOG(ERROR) << "Cannot update record " << record_id
<< " in storage during AuthSession because writing failed.";
}
}
}
void FpcBiometricsManager::OnTaskComplete() {
session_weak_factory_.InvalidateWeakPtrs();
running_task_ = false;
}
bool FpcBiometricsManager::LoadRecord(const std::string& user_id,
const std::string& label,
const std::string& record_id,
const base::Value& data) {
std::string tmpl_data_base64;
if (!data.GetAsString(&tmpl_data_base64)) {
LOG(ERROR) << "Cannot load data string from record " << record_id << ".";
return false;
}
base::StringPiece tmpl_data_base64_sp(tmpl_data_base64);
std::string tmpl_data_str;
base::Base64Decode(tmpl_data_base64_sp, &tmpl_data_str);
std::vector<uint8_t> tmpl_data_vector(tmpl_data_str.begin(),
tmpl_data_str.end());
BioTemplate tmpl(bio_lib_->DeserializeTemplate(tmpl_data_vector));
InternalRecord internal_record = {user_id, label, std::move(tmpl)};
{
base::AutoLock guard(records_lock_);
records_[record_id] = std::move(internal_record);
}
LOG(INFO) << "Load record " << record_id << " from disk.";
return true;
}
bool FpcBiometricsManager::WriteRecord(const BiometricsManager::Record& record,
uint8_t* tmpl_data,
size_t tmpl_size) {
base::StringPiece tmpl_sp(reinterpret_cast<char*>(tmpl_data), tmpl_size);
std::string tmpl_base64;
base::Base64Encode(tmpl_sp, &tmpl_base64);
return biod_storage_.WriteRecord(
record, std::unique_ptr<base::Value>(new base::StringValue(tmpl_base64)));
}
} // namespace biod