From 80917a8b79b0052d07060143fff346eddd189786 Mon Sep 17 00:00:00 2001 From: Simon Rettberg Date: Mon, 25 Mar 2024 17:27:15 +0100 Subject: Add support for QRCode login --- src/qrlogin.cpp | 167 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 src/qrlogin.cpp (limited to 'src/qrlogin.cpp') diff --git a/src/qrlogin.cpp b/src/qrlogin.cpp new file mode 100644 index 0000000..1e44668 --- /dev/null +++ b/src/qrlogin.cpp @@ -0,0 +1,167 @@ +#include "qrlogin.h" +#include "settings.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define SVG_RENDER_SCALE 3 +#define QR_VALID_MS 300000 + +static QString toBase32(const QByteArray &data); + +QrLogin::QrLogin(QObject *parent) + : QObject(parent) + , dest(nullptr) + , timer(new QTimer(this)) + , nam(new QNetworkAccessManager(this)) + , elapsed(new QElapsedTimer()) +{ + QByteArray input; + input.append((const char*)this, sizeof(*this)); + input.append(QString::asprintf("%d %d %d ", QCursor::pos().x(), QCursor::pos().y(), (int)getpid()).toUtf8()); + input.append(QString::number(QDateTime::currentMSecsSinceEpoch()).toUtf8()); + token = toBase32(QCryptographicHash::hash(input, QCryptographicHash::Md5).left(10)).left(16); + nam->setAutoDeleteReplies(true); + nam->setTransferTimeout(5000); +} + +void QrLogin::abort() +{ + QObject::disconnect(timer, nullptr, nullptr, nullptr); + timer->stop(); + timer->blockSignals(true); + nam->blockSignals(true); +} + +void QrLogin::loadQrCode(QLabel *dest) +{ + this->dest = dest; + QObject::disconnect(timer, nullptr, nullptr, nullptr); + timer->stop(); + timer->setSingleShot(true); + svgData.clear(); + QNetworkReply *reply = + nam->get(QNetworkRequest(Settings::shibUrl() + QLatin1String("?action=qrgen&token=") + token)); + connect(reply, &QNetworkReply::readyRead, [=]() { + svgData.append(reply->readAll()); + }); + connect(reply, &QNetworkReply::finished, [reply, this]() { + QObject::disconnect(timer, nullptr, nullptr, nullptr); + timer->stop(); + auto err = reply->error(); + if (err != QNetworkReply::NoError) { + qDebug() << err << svgData; + emit triggerReset(QLatin1String("QRCode Error: ") + QString::fromUtf8(svgData)); + return; + } + // Render at 3x size because QSvgRenderer adds ugly artefacts between pixels/modules + QPixmap pm(this->dest->size() * SVG_RENDER_SCALE); + pm.fill(Qt::white); + QPainter painter(&pm); + QRect square = pm.rect(); + square.setSize(pm.size()); + int diff = square.width() - square.height(); + if (diff > 0) { + square.setLeft(diff / 2); + square.setWidth(square.height()); + } else if (diff < 0) { + square.setTop(-diff / 2); + square.setHeight(square.width()); + } + square.adjust(6 * SVG_RENDER_SCALE, 6 * SVG_RENDER_SCALE, -6 * SVG_RENDER_SCALE, -6 * SVG_RENDER_SCALE); + QSvgRenderer(svgData).render(&painter, square); + painter.end(); + this->dest->setPixmap(pm.scaled(pm.size() / SVG_RENDER_SCALE, Qt::KeepAspectRatio, Qt::SmoothTransformation)); + this->dest->update(); + connect(timer, &QTimer::timeout, [=]() { + if (elapsed->hasExpired(300000)) { + QObject::disconnect(timer, nullptr, nullptr, nullptr); + timer->stop(); + emit triggerReset(QLatin1String("Timeout: QR Code expired")); + return; + } + QNetworkReply *pr = + nam->get(QNetworkRequest(Settings::shibUrl() + QLatin1String("?action=qrpoll&token=") + token)); + connect(pr, &QNetworkReply::finished, [=]() { + int code = pr->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + if (pr->error() != QNetworkReply::NoError) { + QObject::disconnect(timer, nullptr, nullptr, nullptr); + timer->stop(); + emit triggerReset(QString::asprintf("QR Error(%d): ", code) + QString::fromUtf8(pr->readAll())); + return; + } + if (code == 204) { + int rem = (QR_VALID_MS - elapsed->elapsed()) / 1000; + if (rem > 60) { + emit updateStatus(QString::asprintf("Code valid for: ~%d mins", (rem + 30) / 60)); + } else { + if (rem >= 58) { + QPixmap copy = this->dest->pixmap(Qt::ReturnByValue); + QImage img = copy.toImage(); + uchar *b = img.bits(); + for (int i = 0; i < img.sizeInBytes(); ++i) { + if (b[i] < 128) b[i] += 128; + } + this->dest->setPixmap(QPixmap::fromImage(img)); + } + emit updateStatus(QString::asprintf("Code valid for: %d s", rem)); + } + return; + } + QObject::disconnect(timer, nullptr, nullptr, nullptr); + timer->stop(); + auto lines = QString::fromUtf8(pr->readAll()).split('\n'); + if (lines.size() >= 2) { + emit startAuthentication(lines[0], QLatin1String("shib=") + lines[1]); + } else { + emit triggerReset(QLatin1String("QR: Invalid reply received")); + } + }); + }); + elapsed->start(); + timer->setSingleShot(false); + timer->start(2000); + }); + connect(timer, &QTimer::timeout, [=]() { + QObject::disconnect(reply, nullptr, nullptr, nullptr); + QObject::disconnect(timer, nullptr, nullptr, nullptr); + timer->stop(); + reply->abort(); + emit triggerReset(QLatin1String("Timeout: Could not get QR Code")); + }); + timer->start(10000); +} + +static QString toBase32(const QByteArray &data) +{ + static const QString base32Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567_-="; + QString result; + int numBits = 0; + quint16 buffer = 0; + + for (int i = 0; i < data.size(); ++i) { + buffer = (buffer << 8) | static_cast(data[i]); + numBits += 8; + + while (numBits >= 5) { + numBits -= 5; + result += base32Chars[(buffer >> numBits) & 0x1F]; + } + } + + if (numBits > 0) { + buffer <<= (5 - numBits); + result += base32Chars[buffer & 0x1F]; + } + + return result; +} -- cgit v1.2.3-55-g7522