| // 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/modem_qrtr.h" |
| |
| #include <algorithm> |
| #include <array> |
| #include <utility> |
| |
| #include <base/bind.h> |
| #include <base/logging.h> |
| #include <base/strings/string_number_conversions.h> |
| #include <libqrtr.h> |
| |
| #include "hermes/apdu.h" |
| #include "hermes/qmi_uim.h" |
| #include "hermes/sgp_22.h" |
| #include "hermes/socket_qrtr.h" |
| |
| namespace { |
| |
| // As per QMI UIM spec section 2.2 |
| constexpr uint8_t kQmiUimService = 0xB; |
| // TODO(jruthe): this is currently the slot on Cheza, but Esim should be able to |
| // support different slots in the future. |
| constexpr uint8_t kEsimSlot = 0x01; |
| constexpr uint8_t kInvalidChannel = -1; |
| |
| bool CheckMessageSuccess(QmiUimCommand cmd, const uim_qmi_result& qmi_result) { |
| if (qmi_result.result == 0) { |
| return true; |
| } |
| |
| LOG(ERROR) << cmd.ToString() |
| << " response contained error: " << qmi_result.error; |
| return false; |
| } |
| |
| } // namespace |
| |
| namespace hermes { |
| |
| struct ApduTxInfo : public ModemQrtr::TxInfo { |
| explicit ApduTxInfo(CommandApdu apdu, |
| ModemQrtr::ResponseCallback cb = nullptr) |
| : apdu_(std::move(apdu)), callback_(cb) {} |
| CommandApdu apdu_; |
| ModemQrtr::ResponseCallback callback_; |
| }; |
| |
| std::unique_ptr<ModemQrtr> ModemQrtr::Create( |
| std::unique_ptr<SocketInterface> socket, |
| Logger* logger, |
| Executor* executor) { |
| // Open the socket prior to passing to ModemQrtr, such that it always has a |
| // valid socket to write to. |
| if (!socket || !socket->Open()) { |
| LOG(ERROR) << "Failed to open socket"; |
| return nullptr; |
| } |
| return std::unique_ptr<ModemQrtr>( |
| new ModemQrtr(std::move(socket), logger, executor)); |
| } |
| |
| ModemQrtr::ModemQrtr(std::unique_ptr<SocketInterface> socket, |
| Logger* logger, |
| Executor* executor) |
| : extended_apdu_supported_(false), |
| current_transaction_id_(static_cast<uint16_t>(-1)), |
| channel_(kInvalidChannel), |
| slot_(kEsimSlot), |
| socket_(std::move(socket)), |
| buffer_(4096), |
| logger_(logger), |
| executor_(executor) { |
| CHECK(socket_); |
| CHECK(socket_->IsValid()); |
| socket_->SetDataAvailableCallback( |
| base::Bind(&ModemQrtr::OnDataAvailable, base::Unretained(this))); |
| |
| // Set SGP.22 specification version supported by this implementation (this is |
| // not currently constrained by the eUICC we use). |
| spec_version_.set_major(2); |
| spec_version_.set_minor(2); |
| spec_version_.set_revision(0); |
| } |
| |
| ModemQrtr::~ModemQrtr() { |
| Shutdown(); |
| socket_->Close(); |
| } |
| |
| void ModemQrtr::SendApdus(std::vector<lpa::card::Apdu> apdus, |
| ResponseCallback cb) { |
| if (current_state_ == State::kUninitialized) { |
| Initialize(); |
| } |
| for (size_t i = 0; i < apdus.size(); ++i) { |
| ResponseCallback callback = |
| (i == apdus.size() - 1 ? std::move(cb) : nullptr); |
| CommandApdu apdu(static_cast<ApduClass>(apdus[i].cla()), |
| static_cast<ApduInstruction>(apdus[i].ins()), |
| extended_apdu_supported_); |
| apdu.AddData(apdus[i].data()); |
| tx_queue_.push_back( |
| {std::make_unique<ApduTxInfo>(std::move(apdu), std::move(callback)), |
| AllocateId(), QmiUimCommand::kSendApdu}); |
| } |
| // Begin transmitting if we are not already processing a transaction. |
| if (current_state_ == State::kReady) { |
| TransmitFromQueue(); |
| } |
| } |
| |
| bool ModemQrtr::IsSimValidAfterEnable() { |
| return true; |
| } |
| |
| bool ModemQrtr::IsSimValidAfterDisable() { |
| return true; |
| } |
| |
| void ModemQrtr::Initialize() { |
| CHECK(socket_->IsValid()); |
| |
| if (current_state_.IsInitialized()) { |
| LOG(WARNING) |
| << "Attempt to initialize already-initialized ModemQrtr instance"; |
| return; |
| } else if (current_state_ != State::kUninitialized) { |
| LOG(WARNING) |
| << "Attempt to initialize a ModemQrtr instance that is already " |
| << "initializing"; |
| return; |
| } |
| |
| current_state_.Transition(State::kInitializeStarted); |
| // StartService should result in a received QRTR_TYPE_NEW_SERVER |
| // packet. Don't send other packets until that occurs. |
| if (!socket_->StartService(kQmiUimService, 1, 0)) { |
| LOG(ERROR) << "Starting uim service failed during ModemQrtr initialization"; |
| return; |
| } |
| // Place Reset request on the tx queue |
| tx_queue_.push_back( |
| {std::unique_ptr<TxInfo>(), AllocateId(), QmiUimCommand::kReset}); |
| // Place OpenLogicalChannel request on the tx queue |
| tx_queue_.push_back({std::unique_ptr<TxInfo>(), AllocateId(), |
| QmiUimCommand::kOpenLogicalChannel}); |
| } |
| |
| void ModemQrtr::FinalizeInitialization() { |
| if (current_state_ != State::kLogicalChannelOpened) { |
| LOG(ERROR) << "ModemQrtr initialization unsuccessful"; |
| // TODO(akhouderchah) Call lpa library and flush kSendApdu requests from the |
| // queue. |
| Shutdown(); |
| return; |
| } |
| LOG(INFO) << "ModemQrtr initialization successful"; |
| current_state_.Transition(State::kReady); |
| // TODO(akhouderchah) set this based on whether or not Extended Length APDU is |
| // supported. |
| extended_apdu_supported_ = false; |
| } |
| |
| void ModemQrtr::Shutdown() { |
| if (current_state_ != State::kUninitialized && |
| current_state_ != State::kInitializeStarted) { |
| // TODO(akhouderchah) Implement actual shutdown procedure |
| socket_->StopService(kQmiUimService, 1, 0); |
| } |
| current_state_.Transition(State::kUninitialized); |
| } |
| |
| uint16_t ModemQrtr::AllocateId() { |
| // transaction id cannot be 0, but when incrementing by 1, an overflow will |
| // result in this method at some point returning 0. Incrementing by 2 when |
| // transaction_id is initialized as an odd number guarantees us that this |
| // method will never return 0 without special-casing the overflow. |
| current_transaction_id_ += 2; |
| return current_transaction_id_; |
| } |
| |
| ///////////////////////////////////// |
| // Transmit method implementations // |
| ///////////////////////////////////// |
| |
| void ModemQrtr::TransmitFromQueue() { |
| if (tx_queue_.empty()) { |
| return; |
| } |
| |
| bool should_pop = true; |
| switch (tx_queue_[0].uim_type_) { |
| case QmiUimCommand::kReset: |
| TransmitQmiReset(&tx_queue_[0]); |
| break; |
| case QmiUimCommand::kOpenLogicalChannel: |
| TransmitQmiOpenLogicalChannel(&tx_queue_[0]); |
| current_state_.Transition(State::kLogicalChannelPending); |
| break; |
| case QmiUimCommand::kSendApdu: |
| // kSendApdu element will be popped off the queue after the response has |
| // been entirely received. This occurs within |ReceiveQmiSendApdu|. |
| should_pop = false; |
| TransmitQmiSendApdu(&tx_queue_[0]); |
| break; |
| default: |
| LOG(ERROR) << "Unexpected QMI UIM type in ModemQrtr tx queue"; |
| } |
| if (should_pop) { |
| tx_queue_.pop_front(); |
| } |
| } |
| |
| void ModemQrtr::TransmitQmiReset(TxElement* tx_element) { |
| DCHECK(tx_element && tx_element->uim_type_ == QmiUimCommand::kReset); |
| |
| uim_reset_req request; |
| SendCommand(QmiUimCommand::kReset, tx_element->id_, &request, |
| uim_reset_req_ei); |
| } |
| |
| void ModemQrtr::TransmitQmiOpenLogicalChannel(TxElement* tx_element) { |
| DCHECK(tx_element && |
| tx_element->uim_type_ == QmiUimCommand::kOpenLogicalChannel); |
| |
| uim_open_logical_channel_req request; |
| request.slot = slot_; |
| request.aid_valid = true; |
| request.aid_len = kAidIsdr.size(); |
| std::copy(kAidIsdr.begin(), kAidIsdr.end(), request.aid); |
| |
| SendCommand(QmiUimCommand::kOpenLogicalChannel, tx_element->id_, &request, |
| uim_open_logical_channel_req_ei); |
| } |
| |
| void ModemQrtr::TransmitQmiSendApdu(TxElement* tx_element) { |
| DCHECK(tx_element && tx_element->uim_type_ == QmiUimCommand::kSendApdu); |
| |
| // TODO(akhouderchah) we can't avoid the copy when encoding into QMI format, |
| // but there is really no need to have this copy. Fix. |
| uim_send_apdu_req request; |
| request.slot = slot_; |
| request.channel_id_valid = true; |
| request.channel_id = channel_; |
| |
| uint8_t* fragment; |
| ApduTxInfo* apdu = static_cast<ApduTxInfo*>(tx_element->info_.get()); |
| size_t fragment_size = apdu->apdu_.GetNextFragment(&fragment); |
| request.apdu_len = fragment_size; |
| std::copy(fragment, fragment + fragment_size, request.apdu); |
| |
| SendCommand(QmiUimCommand::kSendApdu, tx_element->id_, &request, |
| uim_send_apdu_req_ei); |
| } |
| |
| bool ModemQrtr::SendCommand(QmiUimCommand type, |
| uint16_t id, |
| void* c_struct, |
| qmi_elem_info* ei) { |
| if (current_state_ == State::kWaitingForResponse) { |
| LOG(ERROR) << "ModemQrtr: attempt to send raw buffer when waiting for a " |
| << "response"; |
| return false; |
| } else if (!current_state_.CanSend()) { |
| LOG(ERROR) << "QRTR tried to send buffer in a non-sending state: " |
| << current_state_; |
| return false; |
| } else if (!socket_->IsValid()) { |
| LOG(ERROR) << "ModemQrtr socket is invalid!"; |
| return false; |
| } |
| |
| std::vector<uint8_t> encoded_buffer(kBufferDataSize * 2, 0); |
| qrtr_packet packet; |
| packet.data = encoded_buffer.data(); |
| packet.data_len = encoded_buffer.size(); |
| |
| size_t len = qmi_encode_message( |
| &packet, QMI_REQUEST, static_cast<uint16_t>(type), id, c_struct, ei); |
| if (len < 0) { |
| LOG(ERROR) << "Failed to encode QMI UIM request: " |
| << static_cast<uint16_t>(type); |
| return false; |
| } |
| |
| VLOG(1) << "ModemQrtr sending transaction type " |
| << static_cast<uint16_t>(type) |
| << " with data (size : " << packet.data_len |
| << ") : " << base::HexEncode(packet.data, packet.data_len); |
| int success = socket_->Send(packet.data, packet.data_len, |
| reinterpret_cast<void*>(&metadata_)); |
| if (success < 0) { |
| LOG(ERROR) << "qrtr_sendto failed"; |
| return false; |
| } |
| // If we are sending a command as per the initialization sequence |
| // (e.g. sending an OPEN_LOGICAL_CHANNEL request), we do not want to jump |
| // straight to kWaitingForResponse afterwards. |
| if (current_state_ == State::kReady) { |
| current_state_.Transition(State::kWaitingForResponse); |
| } |
| return true; |
| } |
| |
| //////////////////////////////////// |
| // Receive method implementations // |
| //////////////////////////////////// |
| |
| void ModemQrtr::ProcessQrtrPacket(uint32_t node, uint32_t port, int size) { |
| 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(), size, &qrtr_sock); |
| if (ret < 0) { |
| LOG(ERROR) << "qrtr_decode failed"; |
| return; |
| } |
| |
| switch (pkt.type) { |
| case QRTR_TYPE_NEW_SERVER: |
| VLOG(1) << "Received NEW_SERVER QRTR packet"; |
| if (pkt.service == kQmiUimService && channel_ == kInvalidChannel) { |
| current_state_.Transition(State::kUimStarted); |
| metadata_.node = pkt.node; |
| metadata_.port = pkt.port; |
| } |
| break; |
| case QRTR_TYPE_DATA: |
| if (current_state_ == State::kWaitingForResponse) { |
| current_state_.Transition(State::kReady); |
| } |
| VLOG(1) << "Received data QRTR packet"; |
| ProcessQmiPacket(pkt); |
| break; |
| case QRTR_TYPE_DEL_SERVER: |
| case QRTR_TYPE_HELLO: |
| case QRTR_TYPE_BYE: |
| case QRTR_TYPE_DEL_CLIENT: |
| case QRTR_TYPE_RESUME_TX: |
| case QRTR_TYPE_EXIT: |
| case QRTR_TYPE_PING: |
| case QRTR_TYPE_NEW_LOOKUP: |
| case QRTR_TYPE_DEL_LOOKUP: |
| LOG(INFO) << "Received QRTR packet of type " << pkt.type << ". Ignoring."; |
| break; |
| default: |
| LOG(WARNING) << "Received QRTR packet but did not recognize packet type " |
| << pkt.type << "."; |
| } |
| // If we cannot yet send another request, it is because we are waiting for a |
| // response. After the response is received and processed, the next request |
| // will be sent. |
| if (current_state_.CanSend()) { |
| TransmitFromQueue(); |
| } |
| } |
| |
| void ModemQrtr::ProcessQmiPacket(const qrtr_packet& packet) { |
| uint32_t qmi_type; |
| if (qmi_decode_header(&packet, &qmi_type) < 0) { |
| LOG(ERROR) << "QRTR received invalid QMI packet"; |
| return; |
| } |
| |
| // TODO(akhouderchah) try to avoid the unnecessary copy from *_resp to vector |
| switch (qmi_type) { |
| case QmiUimCommand::kReset: |
| // TODO(akhouderchah) implement a service reset |
| break; |
| case QmiUimCommand::kOpenLogicalChannel: |
| ReceiveQmiOpenLogicalChannel(packet); |
| if (!current_state_.IsInitialized()) { |
| FinalizeInitialization(); |
| } |
| break; |
| case QmiUimCommand::kSendApdu: |
| ReceiveQmiSendApdu(packet); |
| break; |
| default: |
| LOG(WARNING) << "Received QMI packet of unknown type: " << qmi_type; |
| } |
| } |
| |
| void ModemQrtr::ReceiveQmiOpenLogicalChannel(const qrtr_packet& packet) { |
| QmiUimCommand cmd(QmiUimCommand::kOpenLogicalChannel); |
| if (current_state_ != State::kLogicalChannelPending) { |
| LOG(ERROR) << "Received unexpected QMI UIM response: " << cmd.ToString() |
| << " in state " << current_state_; |
| return; |
| } |
| |
| uim_open_logical_channel_resp resp; |
| unsigned int id; |
| if (qmi_decode_message(&resp, &id, &packet, QMI_RESPONSE, cmd, |
| uim_open_logical_channel_resp_ei) < 0) { |
| LOG(ERROR) << "Failed to decode QMI UIM response: " << cmd.ToString(); |
| return; |
| } else if (!CheckMessageSuccess(cmd, resp.result)) { |
| return; |
| } |
| |
| if (!resp.channel_id_valid) { |
| LOG(ERROR) << "QMI UIM response for " << cmd.ToString() |
| << " contained an invalid channel id"; |
| return; |
| } |
| |
| channel_ = resp.channel_id; |
| current_state_.Transition(State::kLogicalChannelOpened); |
| } |
| |
| void ModemQrtr::ReceiveQmiSendApdu(const qrtr_packet& packet) { |
| QmiUimCommand cmd(QmiUimCommand::kSendApdu); |
| CHECK(tx_queue_.size()); |
| // Ensure that the queued element is for a kSendApdu command |
| TxInfo* base_info = tx_queue_[0].info_.get(); |
| CHECK(base_info); |
| CHECK(dynamic_cast<ApduTxInfo*>(base_info)); |
| |
| static ResponseApdu payload; |
| uim_send_apdu_resp resp; |
| unsigned int id; |
| ApduTxInfo* info = static_cast<ApduTxInfo*>(base_info); |
| if (!qmi_decode_message(&resp, &id, &packet, QMI_RESPONSE, cmd, |
| uim_send_apdu_resp_ei)) { |
| LOG(ERROR) << "Failed to decode QMI UIM response: " << cmd.ToString(); |
| return; |
| } else if (!CheckMessageSuccess(cmd, resp.result)) { |
| return; |
| } |
| |
| VLOG(2) << "Adding to payload from APDU response (" |
| << resp.apdu_response_len - 2 << " bytes): " |
| << base::HexEncode(resp.apdu_response, resp.apdu_response_len - 2); |
| payload.AddData(resp.apdu_response, resp.apdu_response_len); |
| if (payload.MorePayloadIncoming()) { |
| // Make the next transmit operation be a request for more APDU data |
| info->apdu_ = payload.CreateGetMoreCommand(false); |
| return; |
| } else if (info->apdu_.HasMoreFragments()) { |
| // Send next fragment of APDU |
| VLOG(1) << "Sending next APDU fragment..."; |
| TransmitFromQueue(); |
| return; |
| } |
| |
| if (tx_queue_.empty() || static_cast<uint16_t>(id) != tx_queue_[0].id_) { |
| LOG(ERROR) << "ModemQrtr received APDU from modem with unrecognized " |
| << "transaction ID"; |
| return; |
| } |
| |
| VLOG(1) << "Finished transaction " << tx_queue_[0].id_ / 2 |
| << " (id: " << tx_queue_[0].id_ << ")"; |
| responses_.push_back(payload.Release()); |
| if (info->callback_) { |
| info->callback_(responses_, lpa::card::EuiccCard::kNoError); |
| // ResponseCallback interface does not indicate a change in ownership of |
| // |responses_|, but all callbacks should transfer ownership. Check for |
| // sanity. |
| CHECK(responses_.empty()); |
| } |
| tx_queue_.pop_front(); |
| } |
| |
| void ModemQrtr::OnDataAvailable(SocketInterface* socket) { |
| CHECK(socket == socket_.get()); |
| |
| void* metadata = nullptr; |
| SocketQrtr::PacketMetadata data = {0, 0}; |
| if (socket->GetType() == SocketInterface::Type::kQrtr) { |
| metadata = reinterpret_cast<void*>(&data); |
| } |
| |
| int bytes_received = socket->Recv(buffer_.data(), buffer_.size(), metadata); |
| if (bytes_received < 0) { |
| LOG(ERROR) << "Socket recv failed"; |
| return; |
| } |
| VLOG(2) << "ModemQrtr recevied raw data (" << bytes_received |
| << " bytes): " << base::HexEncode(buffer_.data(), bytes_received); |
| ProcessQrtrPacket(data.node, data.port, bytes_received); |
| } |
| |
| const lpa::proto::EuiccSpecVersion& ModemQrtr::GetCardVersion() { |
| return spec_version_; |
| } |
| |
| bool ModemQrtr::State::Transition(ModemQrtr::State::Value value) { |
| bool valid_transition = false; |
| switch (value) { |
| case kUninitialized: |
| valid_transition = true; |
| break; |
| case kReady: |
| valid_transition = |
| (value_ == kLogicalChannelOpened || value_ == kWaitingForResponse); |
| break; |
| default: |
| // Most states can only transition from the previous state. |
| valid_transition = (value == value_ + 1); |
| } |
| |
| if (valid_transition) { |
| value_ = value; |
| } else { |
| LOG(ERROR) << "Cannot transition from state " << *this << " to state " |
| << State(value); |
| } |
| return valid_transition; |
| } |
| |
| } // namespace hermes |