blob: b5811feb5fd779c9bf5ec8b4ebda2fae19706126 [file] [log] [blame]
// Copyright 2017 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 <algorithm>
#include <memory>
#include <utility>
#include <base/bind.h>
#include <base/callback.h>
#include <base/strings/string_number_conversions.h>
#include <base/sys_byteorder.h>
#include <base/timer/timer.h>
#include "u2fd/u2fhid.h"
namespace {
// Mandatory length of the U2F HID report.
constexpr size_t kU2fReportSize = 64;
// Size of the payload for an INIT U2F HID report.
constexpr size_t kInitReportPayloadSize = 57;
// Size of the payload for a Continuation U2F HID report.
constexpr size_t kContReportPayloadSize = 59;
// HID frame CMD/SEQ byte definitions.
constexpr uint8_t kFrameTypeMask = 0x80;
constexpr uint8_t kFrameTypeInit = 0x80;
// when bit 7 is not set, the frame type is CONTinuation.
// INIT command parameters
constexpr uint32_t kCidBroadcast = -1U;
constexpr size_t kInitNonceSize = 8;
constexpr uint8_t kInterfaceVersion = 2;
constexpr uint8_t kCapFlagWink = 0x01;
constexpr uint8_t kCapFlagLock = 0x02;
constexpr int kU2fHidTimeoutMs = 500;
constexpr size_t kMaxPayloadSize = (64 - 7 + 128 * (64 - 5)); // 7609 bytes
// Maximum duration one can keep the channel lock as specified by the U2FHID
// specification
constexpr int kMaxLockDurationSeconds = 10;
// Response to the APDU requesting the U2F protocol version
constexpr char kSupportedU2fVersion[] = "U2F_V2";
// HID report descriptor for U2F interface.
constexpr uint8_t kU2fReportDesc[] = {
0x06, 0xD0, 0xF1, /* Usage Page (FIDO Alliance), FIDO_USAGE_PAGE */
0x09, 0x01, /* Usage (U2F HID Auth. Device) FIDO_USAGE_U2FHID */
0xA1, 0x01, /* Collection (Application), HID_APPLICATION */
0x09, 0x20, /* Usage (Input Report Data), FIDO_USAGE_DATA_IN */
0x15, 0x00, /* Logical Minimum (0) */
0x26, 0xFF, 0x00, /* Logical Maximum (255) */
0x75, 0x08, /* Report Size (8) */
0x95, 0x40, /* Report Count (64), HID_INPUT_REPORT_BYTES */
0x81, 0x02, /* Input (Data, Var, Abs), Usage */
0x09, 0x21, /* Usage (Output Report Data), FIDO_USAGE_DATA_OUT */
0x15, 0x00, /* Logical Minimum (0) */
0x26, 0xFF, 0x00, /* Logical Maximum (255) */
0x75, 0x08, /* Report Size (8) */
0x95, 0x40, /* Report Count (64), HID_OUTPUT_REPORT_BYTES */
0x91, 0x02, /* Output (Data, Var, Abs), Usage */
0xC0 /* End Collection */
};
} // namespace
namespace u2f {
// U2FHID Command codes
enum class U2fHid::U2fHidCommand : uint8_t {
kPing = 1,
kMsg = 3,
kLock = 4,
kInit = 6,
kWink = 8,
kError = 0x3f,
};
// U2FHID error codes
enum class U2fHid::U2fHidError : uint8_t {
kNone = 0,
kInvalidCmd = 1,
kInvalidPar = 2,
kInvalidLen = 3,
kInvalidSeq = 4,
kMsgTimeout = 5,
kChannelBusy = 6,
kLockRequired = 10,
kInvalidCid = 11,
kOther = 127,
};
class U2fHid::HidPacket {
public:
explicit HidPacket(const std::string& report);
bool IsValidFrame() const { return valid_; }
bool IsInitFrame() const { return (tcs_ & kFrameTypeMask) == kFrameTypeInit; }
uint32_t ChannelId() const { return cid_; }
U2fHid::U2fHidCommand Command() const {
return static_cast<U2fHidCommand>(tcs_ & ~kFrameTypeMask);
}
uint8_t SeqNumber() const { return tcs_ & ~kFrameTypeMask; }
int PayloadIndex() const { return IsInitFrame() ? 8 : 6; }
size_t MessagePayloadSize() const { return bcnt_; }
private:
bool valid_;
uint32_t cid_; // Channel Identifier
uint8_t tcs_; // type and command or sequence number
uint16_t bcnt_; // payload length as defined by U2fHID specification
};
U2fHid::HidPacket::HidPacket(const std::string& report)
: valid_(false), cid_(0), tcs_(0), bcnt_(0) {
// the report is prefixed by the report ID (we skip it below).
if (report.size() != kU2fReportSize + 1) /* Invalid U2FHID report */
return;
// U2FHID frame bytes parsing.
// As defined in the "FIDO U2F HID Protocol Specification":
// An initialization packet is defined as
//
// Offset Length Mnemonic Description
// 0 4 CID Channel identifier
// 4 1 CMD Command identifier (bit 7 always set)
// 5 1 BCNTH High part of payload length
// 6 1 BCNTL Low part of payload length
// 7 (s - 7) DATA Payload data (s is the fixed packet size)
// The command byte has always the highest bit set to distinguish it
// from a continuation packet, which is described below.
//
// A continuation packet is defined as
//
// Offset Length Mnemonic Description
// 0 4 CID Channel identifier
// 4 1 SEQ Packet sequence 0x00..0x7f (bit 7 always cleared)
// 5 (s - 5) DATA Payload data (s is the fixed packet size)
// With this approach, a message with a payload less or equal to (s - 7)
// may be sent as one packet. A larger message is then divided into one or
// more continuation packets, starting with sequence number 0 which then
// increments by one to a maximum of 127.
// the CID word is not aligned
memcpy(&cid_, &report[1], sizeof(cid_));
tcs_ = report[5];
uint16_t raw_count;
memcpy(&raw_count, &report[6], sizeof(raw_count));
bcnt_ = base::NetToHost16(raw_count);
valid_ = true;
}
class U2fHid::HidMessage {
public:
HidMessage(U2fHidCommand cmd, uint32_t cid) : cid_(cid), cmd_(cmd) {}
// Appends |bytes| to the message payload.
void AddPayload(const std::string& bytes);
// Appends the single |byte| to the message payload.
void AddByte(uint8_t byte);
// Fills an HID report with the part of the message starting at |offset|.
// Returns the offset of the remaining unused content in the message.
int BuildReport(int offset, std::string* report_out);
private:
uint32_t cid_;
U2fHidCommand cmd_;
std::string payload_;
};
void U2fHid::HidMessage::AddPayload(const std::string& bytes) {
payload_ += bytes;
}
void U2fHid::HidMessage::AddByte(uint8_t byte) {
payload_.push_back(byte);
}
int U2fHid::HidMessage::BuildReport(int offset, std::string* report_out) {
size_t data_size;
// Serialize one chunk of the message in a 64-byte HID report
// (see the HID report structure in HidPacket constructor)
report_out->assign(
std::string(reinterpret_cast<char*>(&cid_), sizeof(uint32_t)));
if (offset == 0) { // INIT message
uint16_t bcnt = payload_.size();
report_out->push_back(static_cast<uint8_t>(cmd_) | kFrameTypeInit);
report_out->push_back(bcnt >> 8);
report_out->push_back(bcnt & 0xff);
data_size = kInitReportPayloadSize;
} else { // CONT message
// Insert sequence number.
report_out->push_back((offset - kInitReportPayloadSize) /
kContReportPayloadSize);
data_size = kContReportPayloadSize;
}
data_size = std::min(data_size, payload_.size() - offset);
*report_out += payload_.substr(offset, data_size);
// Ensure the report is 64-B long
report_out->insert(report_out->end(), kU2fReportSize - report_out->size(), 0);
offset += data_size;
VLOG(2) << "TX RPT ["
<< base::HexEncode(report_out->data(), report_out->size()) << "]";
return offset != payload_.size() ? offset : 0;
}
struct U2fHid::Transaction {
uint32_t cid = 0;
U2fHidCommand cmd = U2fHidCommand::kError;
size_t total_size = 0;
int seq = 0;
std::string payload;
base::OneShotTimer timeout;
};
U2fHid::U2fHid(std::unique_ptr<HidInterface> hid,
const TransmitApduCallback& transmit_func,
const IgnoreButtonCallback& ignore_func)
: hid_(std::move(hid)),
transmit_apdu_(transmit_func),
ignore_button_(ignore_func),
free_cid_(1),
locked_cid_(0) {
transaction_ = std::make_unique<Transaction>();
hid_->SetOutputReportHandler(
base::Bind(&U2fHid::ProcessReport, base::Unretained(this)));
}
U2fHid::~U2fHid() = default;
bool U2fHid::Init() {
return hid_->Init(kInterfaceVersion,
std::string(reinterpret_cast<const char*>(kU2fReportDesc),
sizeof(kU2fReportDesc)));
}
bool U2fHid::GetU2fVersion(std::string* version_out) {
std::string ping(8, 0);
std::string ver;
// build the APDU for the command U2F_VERSION:
// CLA INS P1 P2 Le
// 00 03 00 00 00
ping[1] = 0x03;
int rc = transmit_apdu_.Run(ping, &ver);
if (!rc) {
// remove the 16-bit status code at the end
*version_out = ver.substr(0, ver.length() - sizeof(uint16_t));
VLOG(1) << "version " << *version_out;
if (*version_out != kSupportedU2fVersion) {
LOG(WARNING) << "Unsupported U2F version " << *version_out;
return false;
}
}
return !rc;
}
void U2fHid::ReturnError(U2fHidError errcode, uint32_t cid, bool clear) {
HidMessage msg(U2fHidCommand::kError, cid);
msg.AddByte(static_cast<uint8_t>(errcode));
VLOG(1) << "ERROR/" << std::hex << static_cast<int>(errcode)
<< " CID:" << std::hex << cid;
if (clear)
transaction_ = std::make_unique<Transaction>();
std::string report;
msg.BuildReport(0, &report);
hid_->SendReport(report);
}
void U2fHid::TransactionTimeout() {
ReturnError(U2fHidError::kMsgTimeout, transaction_->cid, true);
}
void U2fHid::LockTimeout() {
if (locked_cid_)
LOG(WARNING) << "Cancelled lock CID:" << std::hex << locked_cid_;
locked_cid_ = 0;
}
void U2fHid::ReturnResponse(const std::string& resp) {
HidMessage msg(transaction_->cmd, transaction_->cid);
int offset = 0;
msg.AddPayload(resp);
// Send all the chunks of the message (split in 64-B HID reports)
do {
std::string report;
offset = msg.BuildReport(offset, &report);
hid_->SendReport(report);
} while (offset);
}
void U2fHid::ScanApdu(const std::string& payload) {
if (payload.size() < 5) // Unknown APDU format
return;
// Duration of the user presence persistence on the firmware side
const base::TimeDelta kPresenceTimeout = base::TimeDelta::FromSeconds(10);
// ISO7816-4:2005 APDU format: CLA INS P1 P2 [request data]
char cla = payload[0];
char ins = payload[1];
char control = payload[4];
constexpr char kU2fRegister = 1; // U2F_REGISTER command code
constexpr char kU2fAuthenticate = 2; // U2F_AUTHENTICATE command code
constexpr char kU2fAuthCheckOnly = 0x7; // U2F_AUTH_CHECK_ONLY flags
// Has the client requested the user physical presence ?
if (cla == 0 && (ins == kU2fRegister ||
(ins == kU2fAuthenticate && control != kU2fAuthCheckOnly))) {
brillo::ErrorPtr err;
// Mask the next power button press for the UI
ignore_button_.Run(kPresenceTimeout.ToInternalValue(), &err, -1);
}
}
void U2fHid::CmdInit(uint32_t cid, const std::string& payload) {
HidMessage msg(U2fHidCommand::kInit, cid);
if (payload.size() != kInitNonceSize) {
VLOG(1) << "Payload size " << payload.size();
ReturnError(U2fHidError::kInvalidLen, cid, false);
return;
}
VLOG(1) << "INIT CID:" << std::hex << cid << " NONCE "
<< base::HexEncode(payload.data(), payload.size());
if (cid == kCidBroadcast) { // Allocate Channel ID
cid = free_cid_++;
// Roll-over if needed
if (free_cid_ == kCidBroadcast)
free_cid_ = 1;
}
// Keep the nonce in the first 8 bytes
msg.AddPayload(payload);
std::string serial_cid(reinterpret_cast<char*>(&cid), sizeof(uint32_t));
msg.AddPayload(serial_cid);
// Append the versions : interface / major / minor / build
msg.AddByte(kInterfaceVersion);
msg.AddByte(0);
msg.AddByte(0);
msg.AddByte(0);
// Append Capability flags
// TODO(vpalatin) the Wink command is only outputting a trace for now,
// do a real action or remove it.
msg.AddByte(kCapFlagLock | kCapFlagWink);
std::string report;
msg.BuildReport(0, &report);
hid_->SendReport(report);
}
int U2fHid::CmdPing(std::string* resp) {
VLOG(1) << "PING len " << transaction_->total_size;
// poke U2F version to simulate latency.
std::string version;
GetU2fVersion(&version);
// send back the same content
*resp = transaction_->payload.substr(0, transaction_->total_size);
return transaction_->total_size;
}
int U2fHid::CmdLock(std::string* resp) {
int duration = transaction_->payload[0];
VLOG(1) << "LOCK " << duration << "s CID:" << std::hex << transaction_->cid;
if (duration > kMaxLockDurationSeconds) {
ReturnError(U2fHidError::kInvalidPar, transaction_->cid, true);
return -EINVAL;
}
if (!duration) {
lock_timeout_.Stop();
locked_cid_ = 0;
} else {
locked_cid_ = transaction_->cid;
lock_timeout_.Start(
FROM_HERE,
base::TimeDelta::FromSeconds(duration),
base::Bind(&U2fHid::LockTimeout, base::Unretained(this)));
}
return 0;
}
int U2fHid::CmdWink(std::string* resp) {
LOG(INFO) << "WINK!";
return 0;
}
int U2fHid::CmdMsg(std::string* resp) {
ScanApdu(transaction_->payload);
return transmit_apdu_.Run(transaction_->payload, resp);
}
void U2fHid::ExecuteCmd() {
int rc;
std::string resp;
transaction_->timeout.Stop();
switch (transaction_->cmd) {
case U2fHidCommand::kMsg:
rc = CmdMsg(&resp);
break;
case U2fHidCommand::kPing:
rc = CmdPing(&resp);
break;
case U2fHidCommand::kLock:
rc = CmdLock(&resp);
break;
case U2fHidCommand::kWink:
rc = CmdWink(&resp);
break;
default:
LOG(WARNING) << "Unknown command " << std::hex
<< static_cast<int>(transaction_->cmd);
ReturnError(U2fHidError::kInvalidCmd, transaction_->cid, true);
return;
}
if (rc >= 0)
ReturnResponse(resp);
// we are done with this transaction
transaction_ = std::make_unique<Transaction>();
}
void U2fHid::ProcessReport(const std::string& report) {
HidPacket pkt(report);
VLOG(2) << "RX RPT/" << report.size() << " ["
<< base::HexEncode(report.data(), report.size()) << "]";
if (!pkt.IsValidFrame())
return; // Invalid report
// Check frame validity
if (pkt.ChannelId() == 0) {
VLOG(1) << "No frame should use channel 0";
ReturnError(U2fHidError::kInvalidCid,
pkt.ChannelId(),
pkt.ChannelId() == transaction_->cid);
return;
}
if (pkt.IsInitFrame() && pkt.Command() == U2fHidCommand::kInit) {
if (pkt.ChannelId() == transaction_->cid) {
// Abort an ongoing multi-packet transaction
VLOG(1) << "Transaction cancelled on CID:" << std::hex << pkt.ChannelId();
transaction_ = std::make_unique<Transaction>();
}
// special case: INIT should not interrupt other commands
CmdInit(pkt.ChannelId(), report.substr(pkt.PayloadIndex(), kInitNonceSize));
return;
}
// not an INIT command from here
if (pkt.IsInitFrame()) { // INIT frame type (not the INIT command)
if (pkt.ChannelId() == kCidBroadcast) {
VLOG(1) << "INIT command not on broadcast CID:" << std::hex
<< pkt.ChannelId();
ReturnError(U2fHidError::kInvalidCid, pkt.ChannelId(), false);
return;
}
if (locked_cid_ && pkt.ChannelId() != locked_cid_) {
// somebody else has the lock
VLOG(1) << "channel locked by CID:" << std::hex << locked_cid_;
ReturnError(U2fHidError::kChannelBusy, pkt.ChannelId(), false);
return;
}
if (transaction_->cid && (pkt.ChannelId() != transaction_->cid)) {
VLOG(1) << "channel used by CID:" << std::hex << transaction_->cid;
ReturnError(U2fHidError::kChannelBusy, pkt.ChannelId(), false);
return;
}
if (transaction_->cid) {
VLOG(1) << "CONT frame expected";
ReturnError(U2fHidError::kInvalidSeq, pkt.ChannelId(), true);
return;
}
if (pkt.MessagePayloadSize() > kMaxPayloadSize) {
VLOG(1) << "Invalid length " << pkt.MessagePayloadSize();
ReturnError(U2fHidError::kInvalidLen, pkt.ChannelId(), true);
return;
}
transaction_->timeout.Start(
FROM_HERE,
base::TimeDelta::FromMilliseconds(kU2fHidTimeoutMs),
base::Bind(&U2fHid::TransactionTimeout, base::Unretained(this)));
// record transaction parameters
transaction_->cid = pkt.ChannelId();
transaction_->total_size = pkt.MessagePayloadSize();
transaction_->cmd = pkt.Command();
transaction_->seq = 0;
transaction_->payload =
report.substr(pkt.PayloadIndex(), transaction_->total_size);
} else { // CONT Frame
if (transaction_->cid == 0) {
VLOG(1) << "invalid CONT";
return; // just ignore
}
if (transaction_->seq != pkt.SeqNumber()) {
VLOG(1) << "invalid sequence " << static_cast<int>(pkt.SeqNumber())
<< " != " << transaction_->seq;
ReturnError(U2fHidError::kInvalidSeq,
pkt.ChannelId(),
pkt.ChannelId() == transaction_->cid);
return;
}
// reload timeout
transaction_->timeout.Start(
FROM_HERE,
base::TimeDelta::FromMilliseconds(kU2fHidTimeoutMs),
base::Bind(&U2fHid::TransactionTimeout, base::Unretained(this)));
// record the payload
transaction_->payload += report.substr(pkt.PayloadIndex());
transaction_->seq++;
}
// Are we done with this transaction ?
if (transaction_->payload.size() >= transaction_->total_size)
ExecuteCmd();
}
} // namespace u2f