#include "config.h"
#include "CtapHidDriver.h"
#if ENABLE(WEB_AUTHN)
#include <WebCore/FidoConstants.h>
#include <wtf/RandomNumber.h>
#include <wtf/RunLoop.h>
#include <wtf/Vector.h>
#include <wtf/text/Base64.h>
namespace WebKit {
using namespace fido;
CtapHidDriver::Worker::Worker(UniqueRef<HidConnection>&& connection)
: m_connection(WTFMove(connection))
{
m_connection->initialize();
}
CtapHidDriver::Worker::~Worker()
{
m_connection->terminate();
}
void CtapHidDriver::Worker::transact(fido::FidoHidMessage&& requestMessage, MessageCallback&& callback)
{
ASSERT(m_state == State::Idle);
m_state = State::Write;
m_requestMessage = WTFMove(requestMessage);
m_responseMessage.reset();
m_callback = WTFMove(callback);
m_connection->invalidateCache();
m_connection->send(m_requestMessage->popNextPacket(), [weakThis = makeWeakPtr(*this)](HidConnection::DataSent sent) mutable {
ASSERT(RunLoop::isMain());
if (!weakThis)
return;
weakThis->write(sent);
});
}
void CtapHidDriver::Worker::write(HidConnection::DataSent sent)
{
if (m_state != State::Write)
return;
if (sent != HidConnection::DataSent::Yes) {
m_responseMessage = WTF::nullopt;
returnMessage();
return;
}
if (!m_requestMessage->numPackets()) {
m_state = State::Read;
m_connection->registerDataReceivedCallback([weakThis = makeWeakPtr(*this)](Vector<uint8_t>&& data) mutable {
ASSERT(RunLoop::isMain());
if (!weakThis)
return;
weakThis->read(data);
});
return;
}
m_connection->send(m_requestMessage->popNextPacket(), [weakThis = makeWeakPtr(*this)](HidConnection::DataSent sent) mutable {
ASSERT(RunLoop::isMain());
if (!weakThis)
return;
weakThis->write(sent);
});
}
void CtapHidDriver::Worker::read(const Vector<uint8_t>& data)
{
if (m_state != State::Read)
return;
if (!m_responseMessage) {
m_responseMessage = FidoHidMessage::createFromSerializedData(data);
if (!m_responseMessage || m_responseMessage->channelId() != m_requestMessage->channelId()) {
LOG_ERROR("Couldn't parse a hid init packet: %s", m_responseMessage ? "wrong channel id." : "bad data.");
m_responseMessage.reset();
return;
}
} else {
if (!m_responseMessage->addContinuationPacket(data)) {
LOG_ERROR("Couldn't parse a hid continuation packet.");
m_responseMessage = WTF::nullopt;
returnMessage();
return;
}
}
if (m_responseMessage->messageComplete()) {
if (m_responseMessage->cmd() == FidoHidDeviceCommand::kKeepAlive) {
m_responseMessage.reset();
return;
}
returnMessage();
return;
}
}
void CtapHidDriver::Worker::returnMessage()
{
auto callback = WTFMove(m_callback);
auto message = WTFMove(m_responseMessage);
reset();
callback(WTFMove(message));
}
void CtapHidDriver::Worker::reset()
{
m_connection->unregisterDataReceivedCallback();
m_callback = nullptr;
m_responseMessage = WTF::nullopt;
m_requestMessage = WTF::nullopt;
m_state = State::Idle;
}
void CtapHidDriver::Worker::cancel(fido::FidoHidMessage&& requestMessage)
{
reset();
m_connection->invalidateCache();
ASSERT(requestMessage.numPackets() == 1);
m_connection->sendSync(requestMessage.popNextPacket());
}
CtapHidDriver::CtapHidDriver(UniqueRef<HidConnection>&& connection)
: m_worker(makeUniqueRef<Worker>(WTFMove(connection)))
, m_nonce(kHidInitNonceLength)
{
}
void CtapHidDriver::transact(Vector<uint8_t>&& data, ResponseCallback&& callback)
{
ASSERT(m_state == State::Idle);
m_state = State::AllocateChannel;
m_channelId = kHidBroadcastChannel;
m_requestData = WTFMove(data);
m_responseCallback = WTFMove(callback);
size_t steps = kHidInitNonceLength / sizeof(uint32_t);
ASSERT(!(kHidInitNonceLength % sizeof(uint32_t)) && steps >= 1);
for (size_t i = 0; i < steps; ++i) {
uint32_t weakRandom = weakRandomUint32();
memcpy(m_nonce.data() + i * sizeof(uint32_t), &weakRandom, sizeof(uint32_t));
}
auto initCommand = FidoHidMessage::create(m_channelId, FidoHidDeviceCommand::kInit, m_nonce);
ASSERT(initCommand);
m_worker->transact(WTFMove(*initCommand), [weakThis = makeWeakPtr(*this)](Optional<FidoHidMessage>&& response) mutable {
ASSERT(RunLoop::isMain());
if (!weakThis)
return;
weakThis->continueAfterChannelAllocated(WTFMove(response));
});
}
void CtapHidDriver::continueAfterChannelAllocated(Optional<FidoHidMessage>&& message)
{
if (m_state != State::AllocateChannel)
return;
if (!message) {
returnResponse({ });
return;
}
ASSERT(message->channelId() == m_channelId);
auto payload = message->getMessagePayload();
ASSERT(payload.size() == kHidInitResponseSize);
if (memcmp(payload.data(), m_nonce.data(), m_nonce.size())) {
m_state = State::Idle;
RunLoop::main().dispatch([weakThis = makeWeakPtr(*this), data = WTFMove(m_requestData), callback = WTFMove(m_responseCallback)]() mutable {
if (!weakThis)
return;
weakThis->transact(WTFMove(data), WTFMove(callback));
});
return;
}
m_state = State::Ready;
auto index = kHidInitNonceLength;
m_channelId = static_cast<uint32_t>(payload[index++]) << 24;
m_channelId |= static_cast<uint32_t>(payload[index++]) << 16;
m_channelId |= static_cast<uint32_t>(payload[index++]) << 8;
m_channelId |= static_cast<uint32_t>(payload[index]);
auto cmd = FidoHidMessage::create(m_channelId, protocol() == ProtocolVersion::kCtap ? FidoHidDeviceCommand::kCbor : FidoHidDeviceCommand::kMsg, m_requestData);
ASSERT(cmd);
m_worker->transact(WTFMove(*cmd), [weakThis = makeWeakPtr(*this)](Optional<FidoHidMessage>&& response) mutable {
ASSERT(RunLoop::isMain());
if (!weakThis)
return;
weakThis->continueAfterResponseReceived(WTFMove(response));
});
}
void CtapHidDriver::continueAfterResponseReceived(Optional<fido::FidoHidMessage>&& message)
{
if (m_state != State::Ready)
return;
ASSERT(!message || message->channelId() == m_channelId);
returnResponse(message ? message->getMessagePayload() : Vector<uint8_t>());
}
void CtapHidDriver::returnResponse(Vector<uint8_t>&& response)
{
auto responseCallback = WTFMove(m_responseCallback);
reset();
responseCallback(WTFMove(response));
}
void CtapHidDriver::reset()
{
m_responseCallback = nullptr;
m_channelId = fido::kHidBroadcastChannel;
m_state = State::Idle;
}
void CtapHidDriver::cancel()
{
if (m_state == State::Idle || protocol() != ProtocolVersion::kCtap)
return;
if (m_state == State::Ready) {
auto cancelCommand = FidoHidMessage::create(m_channelId, FidoHidDeviceCommand::kCancel, { });
m_worker->cancel(WTFMove(*cancelCommand));
}
reset();
}
}
#endif // ENABLE(WEB_AUTHN)