blob: e9986de6fcc05b2eb6462603bbee56f64d5bdf06 [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 "hermes/esim.h"
#include <algorithm>
#include <iterator>
#include <utility>
#include <vector>
#include <base/bind.h>
#include <base/logging.h>
#include <base/memory/ptr_util.h>
#include <base/strings/string_number_conversions.h>
namespace {
// This allows testing of Esim without actually needing to open a real
// QRTR socket to a QRTR modem.
bool CreateSocketPair(base::ScopedFD* one, base::ScopedFD* two) {
int raw_socks[2];
if (socketpair(AF_UNIX, SOCK_SEQPACKET, 0, raw_socks) != 0) {
PLOG(ERROR) << "Failed to create socket pair.";
return false;
}
one->reset(raw_socks[0]);
two->reset(raw_socks[1]);
return true;
}
// Convert ASCII-encoded hex character to its corresponding digit
bool HexToDigit(uint8_t hex_char, uint8_t* digit) {
DLOG_ASSERT(digit);
if ('0' <= hex_char && hex_char <= '9') {
*digit = hex_char - '0';
} else if ('A' <= hex_char && hex_char <= 'F') {
*digit = hex_char - 'A' + 10;
} else if ('a' <= hex_char && hex_char <= 'f') {
*digit = hex_char - 'a' + 10;
} else {
return false;
}
return true;
}
} // namespace
namespace hermes {
// Maximum size of an APDU data packet.
constexpr size_t kMaxApduDataSize = 255;
// P1 byte based on the length of the data field P3 in a transport command.
constexpr uint8_t kP1MoreBlocks = 0x11;
constexpr uint8_t kP1LastBlock = 0x91;
// Status byte for response okay as defined in ISO 7816.
constexpr uint8_t kApduStatusOkay = 0x90;
// Status byte for more response as defined in ISO 7816.
constexpr uint8_t kApduStatusMoreResponse = 0x61;
// Command code to request more bytes from the chip.
constexpr uint8_t kGetMoreResponseCommand = 0xC0;
// Store data class as defined in ISO 7816.
constexpr uint8_t kClaStoreData = 0x80;
// Store data instruction as defined in ISO 7816.
constexpr uint8_t kInsStoreData = 0xE2;
// Indicator that the length field will be two bytes, instead of one as defined
// in ISO 7816.
constexpr uint8_t kTwoByteLengthIndicator = 0x82;
// Le byte as defined in SGP.22
constexpr uint8_t kLeByte = 0x00;
// ASN.1 Tags for primitive types
constexpr uint8_t kAsn1TagCtx0 = 0x80;
constexpr uint8_t kAsn1TagCtx2 = 0x82;
// ASN.1 Tags for constructed types
constexpr uint8_t kAsn1TagCtxCmp0 = 0xA0;
constexpr uint8_t kAsn1TagCtxCmp1 = 0xA1;
// Application identifier for opening a logical channel to the eSIM slot
constexpr std::array<uint8_t, 16> kIsdrAid = {
0xA0, 0x00, 0x00, 0x05, 0x59, 0x10, 0x10, 0xFF,
0xFF, 0xFF, 0xFF, 0x89, 0x00, 0x00, 0x01, 0x00,
};
Esim::Esim(uint8_t slot,
const std::string& imei,
const std::string& matching_id,
base::ScopedFD fd)
: watcher_(FROM_HERE),
current_transaction_(1),
imei_(imei),
matching_id_(matching_id),
slot_(slot),
buffer_(4096),
node_(-1),
port_(-1),
channel_(-1),
qrtr_socket_fd_(std::move(fd)),
weak_factory_(this) {}
Esim::TransactionCallback::TransactionCallback(
const DataCallback& data_callback, const ErrorCallback& error_callback)
: data_callback(data_callback), error_callback(error_callback) {}
void Esim::Initialize(const base::Closure& success_callback,
const ErrorCallback& error_callback) {
bool ret = base::MessageLoopForIO::current()->WatchFileDescriptor(
qrtr_socket_fd_.get(), true /* persistant */,
base::MessageLoopForIO::WATCH_READ, &watcher_, this);
CHECK(ret);
int success = qrtr_new_lookup(qrtr_socket_fd_.get(), kQrtrUimService,
1 /* version */, 0 /* instance */);
if (success < 0) {
LOG(ERROR) << __func__ << ": qrtr_new_lookup failed";
error_callback.Run(EsimError::kEsimError);
return;
}
initialize_callback_ = success_callback;
}
// static
std::unique_ptr<Esim> Esim::Create(std::string imei,
std::string matching_id) {
base::ScopedFD fd(qrtr_open(kQrtrPort));
if (!fd.is_valid()) {
LOG(ERROR) << __func__ << ": Could not open socket";
return nullptr;
}
VLOG(1) << "Constructing Esim object with slot : "
<< static_cast<int>(kEsimSlot) << " and imei " << imei;
return base::WrapUnique(
new Esim(kEsimSlot, imei, matching_id, std::move(fd)));
}
// static
std::unique_ptr<Esim> Esim::CreateForTest(
base::ScopedFD* sock, std::string imei, std::string matching_id) {
base::ScopedFD fd;
if (!CreateSocketPair(&fd, sock)) {
LOG(ERROR) << __func__ << ": Could not open socket";
return nullptr;
}
VLOG(1) << __func__ << ": Constructing test Esim object with slot : "
<< static_cast<int>(kEsimSlot) << " and imei " << imei;
return base::WrapUnique(
new Esim(kEsimSlot, imei, matching_id, std::move(fd)));
}
void Esim::OpenLogicalChannel(const DataCallback& data_callback,
const ErrorCallback& error_callback) {
std::vector<uint8_t> raw_buffer(kBufferDataSize, 0);
qrtr_packet buffer;
buffer.data_len = raw_buffer.size();
buffer.data = raw_buffer.data();
uim_open_logical_channel_req request;
request.slot = slot_;
request.aid_valid = true;
request.aid_len = kIsdrAid.size();
std::copy(kIsdrAid.begin(), kIsdrAid.end(), request.aid);
size_t len = qmi_encode_message(
&buffer, QMI_REQUEST,
static_cast<uint32_t>(QmiUimCommand::kOpenLogicalChannel),
current_transaction_, &request, uim_open_logical_channel_req_ei);
if (len < 0) {
LOG(ERROR) << __func__ << ": qmi_encode_message failed";
error_callback.Run(EsimError::kEsimError);
return;
}
VLOG(1) << __func__
<< ": Initiating OpenLogicalChannel Transaction with request (size : "
<< buffer.data_len
<< ") : " << base::HexEncode(buffer.data, buffer.data_len);
InitiateTransaction(buffer, data_callback, error_callback);
}
void Esim::GetInfo(int which,
const DataCallback& data_callback,
const ErrorCallback& error_callback) {
if (!qrtr_socket_fd_.is_valid()) {
LOG(ERROR) << __func__ << ": File Descriptor to QRTR invalid";
error_callback.Run(EsimError::kEsimNotConnected);
return;
}
const std::vector<uint8_t> get_info_request = {
static_cast<uint8_t>((which >> 8) & 0xFF),
static_cast<uint8_t>(which & 0xFF),
0x00, // APDU length
};
payload_.clear();
VLOG(1) << __func__
<< ": Added GetInfo APDU (size : " << get_info_request.size()
<< ") to queue : "
<< base::HexEncode(get_info_request.data(), get_info_request.size());
QueueStoreData(get_info_request);
SendApdu(data_callback, error_callback);
}
void Esim::GetChallenge(const DataCallback& data_callback,
const ErrorCallback& error_callback) {
if (!qrtr_socket_fd_.is_valid()) {
LOG(ERROR) << __func__ << ": File Descriptor to QRTR invalid";
error_callback.Run(EsimError::kEsimNotConnected);
return;
}
const std::vector<uint8_t> get_challenge_request = {
static_cast<uint8_t>((kEsimChallengeTag >> 8) & 0xFF),
static_cast<uint8_t>(kEsimChallengeTag & 0xFF),
0x00, // APDU Length
};
payload_.clear();
VLOG(1) << __func__ << ": Added GetChallenge APDU (size : "
<< get_challenge_request.size() << ") to queue : "
<< base::HexEncode(get_challenge_request.data(),
get_challenge_request.size());
QueueStoreData(get_challenge_request);
SendApdu(data_callback, error_callback);
}
// TODO(jruthe): pass |server_data| to Esim::SendEsimMessage to make
// correct libqrtr call to the eSIM chip.
void Esim::AuthenticateServer(
const std::vector<uint8_t>& server_signed1,
const std::vector<uint8_t>& server_signature,
const std::vector<uint8_t>& public_key,
const std::vector<uint8_t>& server_certificate,
const DataCallback& data_callback,
const ErrorCallback& error_callback) {
if (!qrtr_socket_fd_.is_valid()) {
LOG(ERROR) << __func__ << ": File Descriptor to QRTR invalid";
error_callback.Run(EsimError::kEsimNotConnected);
return;
}
std::vector<uint8_t> ctx_params;
if (!ConstructCtxParams(&ctx_params)) {
LOG(ERROR) << __func__ << ": failed to construct ctx_params";
error_callback.Run(EsimError::kEsimError);
return;
}
size_t payload_size = server_signed1.size() + server_signature.size() +
public_key.size() + server_certificate.size() +
ctx_params.size();
std::vector<uint8_t> raw_data_buffer;
raw_data_buffer.push_back(
static_cast<uint8_t>((kAuthenticateServerTag >> 8) & 0xFF));
raw_data_buffer.push_back(
static_cast<uint8_t>(kAuthenticateServerTag & 0xFF));
raw_data_buffer.push_back(kTwoByteLengthIndicator);
raw_data_buffer.push_back(static_cast<uint8_t>((payload_size >> 8) & 0xFF));
raw_data_buffer.push_back(static_cast<uint8_t>(payload_size & 0xFF));
raw_data_buffer.insert(raw_data_buffer.end(), server_signed1.begin(),
server_signed1.end());
raw_data_buffer.insert(raw_data_buffer.end(), server_signature.begin(),
server_signature.end());
raw_data_buffer.insert(raw_data_buffer.end(), public_key.begin(),
public_key.end());
raw_data_buffer.insert(raw_data_buffer.end(), server_certificate.begin(),
server_certificate.end());
raw_data_buffer.insert(raw_data_buffer.end(), ctx_params.begin(),
ctx_params.end());
payload_.clear();
QueueStoreData(raw_data_buffer);
SendApdu(data_callback, error_callback);
}
void Esim::QueueStoreData(const std::vector<uint8_t>& payload) {
FragmentAndQueueApdu(kClaStoreData, kInsStoreData, payload);
}
void Esim::PrepareDownloadRequest(
const std::vector<uint8_t>& smdp_signed2,
const std::vector<uint8_t>& smdp_signature2,
const std::vector<uint8_t>& smdp_certificate,
const DataCallback& data_callback,
const ErrorCallback& error_callback) {
if (!qrtr_socket_fd_.is_valid()) {
LOG(ERROR) << __func__ << ": File Descriptor to QRTR invalid";
error_callback.Run(EsimError::kEsimNotConnected);
return;
}
size_t payload_size =
smdp_signed2.size() + smdp_signature2.size() + smdp_certificate.size();
std::vector<uint8_t> raw_data_buffer;
raw_data_buffer.push_back(
static_cast<uint8_t>((kPrepareDownloadRequestTag >> 8) & 0xFF));
raw_data_buffer.push_back(
static_cast<uint8_t>(kPrepareDownloadRequestTag & 0xFF));
// Magic number
raw_data_buffer.push_back(0x82);
raw_data_buffer.push_back(static_cast<uint8_t>((payload_size >> 8) & 0xFF));
raw_data_buffer.push_back(static_cast<uint8_t>(payload_size & 0xFF));
raw_data_buffer.insert(raw_data_buffer.end(), smdp_signed2.begin(),
smdp_signed2.end());
raw_data_buffer.insert(raw_data_buffer.end(), smdp_signature2.begin(),
smdp_signature2.end());
raw_data_buffer.insert(raw_data_buffer.end(), smdp_certificate.begin(),
smdp_certificate.end());
payload_.clear();
QueueStoreData(raw_data_buffer);
SendApdu(data_callback, error_callback);
}
void Esim::LoadBoundProfilePackage(
const std::vector<uint8_t>& bound_profile_package,
const DataCallback& data_callback,
const ErrorCallback& error_callback) {
if (!qrtr_socket_fd_.is_valid()) {
LOG(ERROR) << __func__ << ": File Descriptor to QRTR invalid";
error_callback.Run(EsimError::kEsimNotConnected);
return;
}
payload_.clear();
QueueStoreData(bound_profile_package);
SendApdu(data_callback, error_callback);
}
void Esim::FragmentAndQueueApdu(
uint8_t cla, uint8_t ins, const std::vector<uint8_t>& apdu_payload) {
size_t total_packets = std::max(1u,
(apdu_payload.size() + kMaxApduDataSize - 1) / kMaxApduDataSize);
VLOG(1) << __func__ << ": constructing " << total_packets << " packets";
auto front_iterator = apdu_payload.begin();
size_t i;
for (i = 0; i < total_packets - 1; ++i) {
std::vector<uint8_t> buf{cla, ins, kP1MoreBlocks, static_cast<uint8_t>(i),
kMaxApduDataSize};
buf.reserve(kMaxApduDataSize + 5);
buf.insert(buf.end(), front_iterator, front_iterator + kMaxApduDataSize);
VLOG(1) << __func__ << ": Queuing APDU fragment (size : " << buf.size()
<< ") : " << base::HexEncode(buf.data(), buf.size());
apdu_queue_.push_back(std::move(buf));
std::advance(front_iterator, kMaxApduDataSize);
}
uint8_t final_packet_size = apdu_payload.end() - front_iterator;
std::vector<uint8_t> buf{cla, ins, kP1LastBlock, static_cast<uint8_t>(i),
final_packet_size};
buf.reserve(final_packet_size + 6);
buf.insert(buf.end(), front_iterator, front_iterator + final_packet_size);
buf.push_back(kLeByte);
VLOG(1) << __func__ << ": Queuing final APDU fragment (size : " << buf.size()
<< ") : " << base::HexEncode(buf.data(), buf.size());
apdu_queue_.push_back(std::move(buf));
}
void Esim::SendApdu(const DataCallback& data_callback,
const ErrorCallback& error_callback) {
if (apdu_queue_.empty()) {
LOG(ERROR) << __func__ << ": called with empty queue";
error_callback.Run(EsimError::kEsimError);
return;
}
std::vector<uint8_t> raw_buffer(kMaxApduDataSize * 2);
std::vector<uint8_t> data_buffer = apdu_queue_.front();
apdu_queue_.pop_front();
qrtr_packet buffer;
buffer.data = raw_buffer.data();
buffer.data_len = raw_buffer.size();
uim_send_apdu_req request;
request.slot = slot_;
request.channel_id_valid = true;
request.channel_id = channel_;
request.apdu_len = data_buffer.size();
std::copy(data_buffer.begin(), data_buffer.end(), request.apdu);
size_t len = qmi_encode_message(
&buffer, QMI_REQUEST, static_cast<uint32_t>(QmiUimCommand::kSendApdu),
current_transaction_, &request, uim_send_apdu_req_ei);
if (len < 0) {
LOG(ERROR) << __func__ << ": qmi_encode_message failed";
error_callback.Run(EsimError::kEsimError);
return;
}
VLOG(1) << __func__ << ": Initiating APDU Transaction with buffer (size : "
<< buffer.data_len
<< ") : " << base::HexEncode(buffer.data, buffer.data_len);
InitiateTransaction(buffer, data_callback, error_callback);
}
void Esim::InitiateTransaction(const qrtr_packet& packet,
const DataCallback& data_callback,
const ErrorCallback& error_callback) {
int bytes_sent = qrtr_sendto(qrtr_socket_fd_.get(), node_, port_, packet.data,
packet.data_len);
if (bytes_sent < 0) {
LOG(ERROR) << __func__ << ": qrtr_sendto failed";
error_callback.Run(EsimError::kEsimError);
return;
}
VLOG(1) << __func__ << ": Packet sent to eSIM, saving callbacks";
response_callbacks_[current_transaction_++] =
TransactionCallback(data_callback, error_callback);
}
void Esim::FinalizeTransaction(const qrtr_packet& packet) {
uint32_t qmi_type;
int ret = qmi_decode_header(&packet, &qmi_type);
if (ret < 0) {
LOG(ERROR) << __func__ << ": Got invalid data packet.";
return;
}
uint32_t transaction_number = GetTransactionNumber(packet);
if (transaction_number > current_transaction_ || transaction_number == 0) {
LOG(ERROR) << __func__
<< ": Got invalid transaction number : " << transaction_number;
return;
}
const auto& callbacks = response_callbacks_.find(transaction_number);
if (callbacks == response_callbacks_.end()) {
LOG(ERROR) << __func__ << ": Couldn't find transaction "
<< transaction_number;
return;
}
const TransactionCallback& transaction_callbacks = callbacks->second;
switch (qmi_type) {
case static_cast<uint32_t>(QmiUimCommand::kOpenLogicalChannel): {
uim_open_logical_channel_resp resp;
qmi_decode_message(
&resp, &transaction_number, &packet, QMI_RESPONSE,
static_cast<uint32_t>(QmiUimCommand::kOpenLogicalChannel),
uim_open_logical_channel_resp_ei);
if (!ResponseSuccess(resp.result.result)) {
LOG(ERROR) << "QmiUimCommand::kOpenLogicalChannel failed.";
// TODO(jruthe): maybe change this to clear instead?
response_callbacks_.erase(callbacks);
transaction_callbacks.error_callback.Run(EsimError::kEsimError);
return;
}
if (resp.channel_id_valid) {
channel_ = resp.channel_id;
} else {
LOG(ERROR) << "Invalid channel_id.";
// TODO(jruthe): ditto to above?
response_callbacks_.erase(callbacks);
transaction_callbacks.error_callback.Run(EsimError::kEsimError);
return;
}
transaction_callbacks.data_callback.Run(std::vector<uint8_t>());
break;
}
case static_cast<uint32_t>(QmiUimCommand::kSendApdu): {
uim_send_apdu_resp resp;
qmi_decode_message(&resp, &transaction_number, &packet, QMI_RESPONSE,
static_cast<uint32_t>(QmiUimCommand::kSendApdu),
uim_send_apdu_resp_ei);
if (!ResponseSuccess(resp.result.result)) {
LOG(ERROR) << __func__ << ": APDU response invalid.";
// TODO(jruthe): ditto
response_callbacks_.erase(callbacks);
transaction_callbacks.error_callback.Run(EsimError::kEsimError);
return;
}
payload_.insert(payload_.end(), resp.apdu_response,
resp.apdu_response + resp.apdu_response_len);
sw1_ = payload_[payload_.size() - 2];
sw2_ = payload_[payload_.size() - 1];
payload_.pop_back();
payload_.pop_back();
if (MorePayloadIncoming()) {
const std::vector<uint8_t> get_more_request = {
static_cast<uint8_t>(kClaStoreData | channel_),
kGetMoreResponseCommand, 0x00, 0x00, sw2_};
apdu_queue_.push_back(get_more_request);
SendApdu(transaction_callbacks.data_callback,
transaction_callbacks.error_callback);
return;
} else if (payload_.empty() && sw1_ == kApduStatusOkay) {
SendApdu(transaction_callbacks.data_callback,
transaction_callbacks.error_callback);
return;
}
transaction_callbacks.data_callback.Run(payload_);
response_callbacks_.erase(callbacks);
break;
}
case static_cast<uint32_t>(QmiUimCommand::kReset):
break;
default:
LOG(WARNING) << __func__ << ": Unknown QMI data type: " << qmi_type;
break;
}
}
bool Esim::StringToBcdBytes(const std::string& source,
std::vector<uint8_t>* dest) {
DLOG_ASSERT(dest);
dest->reserve((source.size() + 1) / 2);
size_t i = 0;
uint8_t msb, lsb;
for (; i < source.size() / 2; ++i) {
if (!HexToDigit(source[2*i], &lsb) ||
!HexToDigit(source[2*i + 1], &msb)) {
return false;
}
dest->push_back((msb << 4) | lsb);
}
if (source.size() % 2) {
if (!HexToDigit(source[2*i - 1], &lsb)) {
return false;
}
dest->push_back(lsb);
}
return true;
}
bool Esim::ConstructCtxParams(std::vector<uint8_t>* ctx_params) {
const std::vector<uint8_t> device_caps = {
0x80, 0x03, 0x0D, 0x00, 0x00, 0x81, 0x03, 0x0D, 0x00, 0x00,
0x85, 0x03, 0x0D, 0x00, 0x00, 0x87, 0x03, 0x02, 0x02, 0x00,
};
std::vector<uint8_t> imei_bytes;
if (!StringToBcdBytes(imei_, &imei_bytes)) {
LOG(ERROR) << __func__ << ": failed to convert imei (" << imei_
<< ") to BCD format";
return false;
}
std::vector<uint8_t> tac(imei_bytes.begin(), imei_bytes.begin() + 4);
VLOG(1) << "converted imei : " << base::HexEncode(imei_.data(), imei_.size());
std::vector<uint8_t> matching_id_bytes;
if (!StringToBcdBytes(matching_id_, &matching_id_bytes)) {
LOG(ERROR) << __func__ << ": failed to convert matching_id ("
<< matching_id_ << ") to BCD format";
return false;
}
uint8_t total_len = matching_id_bytes.size() + device_caps.size() +
imei_bytes.size() + tac.size() + 10;
ctx_params->push_back(kAsn1TagCtxCmp0);
ctx_params->push_back(total_len);
ctx_params->push_back(kAsn1TagCtx0);
ctx_params->push_back(static_cast<uint8_t>(matching_id_bytes.size()));
ctx_params->insert(ctx_params->end(), matching_id_bytes.begin(),
matching_id_bytes.end());
ctx_params->push_back(kAsn1TagCtxCmp1);
ctx_params->push_back(
static_cast<uint8_t>(tac.size() + device_caps.size() + imei_.size()) + 6);
ctx_params->push_back(kAsn1TagCtx0);
ctx_params->push_back(static_cast<uint8_t>(tac.size()));
ctx_params->insert(ctx_params->end(), tac.begin(), tac.end());
ctx_params->push_back(kAsn1TagCtxCmp0);
ctx_params->push_back(static_cast<uint8_t>(device_caps.size()));
ctx_params->insert(ctx_params->end(), device_caps.begin(), device_caps.end());
ctx_params->push_back(kAsn1TagCtx2);
ctx_params->push_back(static_cast<uint8_t>(imei_.size()));
ctx_params->insert(ctx_params->end(), imei_.begin(), imei_.end());
VLOG(1) << "ctxParams : "
<< base::HexEncode(ctx_params->data(), ctx_params->size());
return true;
}
uint16_t Esim::GetTransactionNumber(const qrtr_packet& packet) const {
if (packet.data_len < 3)
return 0;
const uint8_t* data = static_cast<uint8_t*>(packet.data);
return ((data[2] << 8) | (data[1]));
}
bool Esim::ResponseSuccess(uint16_t response) const {
return (response == 0);
}
bool Esim::MorePayloadIncoming() const {
return (sw1_ == kApduStatusMoreResponse);
}
uint8_t Esim::GetNextPayloadSize() const {
return sw2_;
}
void Esim::OnFileCanReadWithoutBlocking(int fd) {
DCHECK_EQ(qrtr_socket_fd_.get(), fd);
uint32_t node, port;
int bytes_received = qrtr_recvfrom(qrtr_socket_fd_.get(), buffer_.data(),
buffer_.size(), &node, &port);
if (bytes_received < 0) {
LOG(ERROR) << __func__ << ": Recvfrom failed.";
return;
}
sockaddr_qrtr qrtr_sock;
qrtr_sock.sq_family = AF_QIPCRTR;
qrtr_sock.sq_node = node;
qrtr_sock.sq_port = port;
qrtr_packet pkt;
int ret = qrtr_decode(&pkt, buffer_.data(), bytes_received, &qrtr_sock);
if (ret < 0) {
LOG(ERROR) << __func__ << ": Qrtr_decode failed.";
return;
}
// TODO(jruthe): parse qrtr packet into different responses
switch (pkt.type) {
case QRTR_TYPE_NEW_SERVER:
if (pkt.service == kQrtrUimService && channel_ == kInvalidChannel) {
port_ = pkt.port;
node_ = pkt.node;
initialize_callback_.Run();
}
break;
case QRTR_TYPE_DATA:
VLOG(1)
<< __func__
<< ": calling Esim::FinalizeTransaction with packet (size : "
<< pkt.data_len << ") : " << base::HexEncode(pkt.data, pkt.data_len);
FinalizeTransaction(pkt);
break;
default:
LOG(WARNING) << __func__ << ": Unkown QRTR packet type: " << pkt.type;
break;
}
}
void Esim::OnFileCanWriteWithoutBlocking(int fd) {
NOTREACHED();
}
} // namespace hermes