283 lines
9.1 KiB
C++
283 lines
9.1 KiB
C++
/*
|
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
* SPDX-FileCopyrightText: 2020 Johan Ouwerkerk <jm.ouwerkerk@gmail.com>
|
|
*/
|
|
#include "uri.h"
|
|
|
|
#include "../base32/base32.h"
|
|
#include "../logging_p.h"
|
|
|
|
#include <QGlobalStatic>
|
|
#include <QScopedPointer>
|
|
#include <QTextCodec>
|
|
|
|
KEYSMITH_LOGGER(logger, ".uri")
|
|
|
|
namespace uri
|
|
{
|
|
static bool isHexDigit(const char digit)
|
|
{
|
|
return (digit >= '0' && digit <= '9') || (digit >= 'A' && digit <= 'F') || (digit >= 'a' && digit <= 'f');
|
|
}
|
|
|
|
std::optional<QByteArray> fromPercentEncoding(const QByteArray &encoded)
|
|
{
|
|
QByteArray decoded(encoded);
|
|
|
|
int index = 0;
|
|
for(index = decoded.indexOf('%', index); index >= 0; index = decoded.indexOf('%', index + 1)) {
|
|
if (decoded.size() < (index + 2) || !isHexDigit(decoded[index + 1]) || !isHexDigit(decoded[index + 2])) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
QByteArray substitute = QByteArray::fromHex(decoded.mid(index + 1, 2));
|
|
decoded.replace(index, 3, substitute);
|
|
}
|
|
|
|
return std::optional<QByteArray>(decoded);
|
|
}
|
|
|
|
static std::optional<QString> convertUtf8(const QByteArray &data)
|
|
{
|
|
static QTextCodec *codec = QTextCodec::codecForName("UTF-8");
|
|
if (!codec) {
|
|
qCDebug(logger) << "Unable to decode data: unable to retrieve codec for UTF-8";
|
|
return std::nullopt;
|
|
}
|
|
|
|
QTextCodec::ConverterState state;
|
|
QString result = codec->toUnicode(data.constData(), data.size(), &state);
|
|
return state.invalidChars == 0 && state.remainingChars == 0 ? std::optional<QString>(result) : std::nullopt;
|
|
}
|
|
|
|
std::optional<QString> decodePercentEncoding(const QByteArray &utf8Data)
|
|
{
|
|
const auto decoded = fromPercentEncoding(utf8Data);
|
|
return decoded ? convertUtf8(*decoded) : std::nullopt;
|
|
}
|
|
|
|
static bool tryDecodeParam(const QByteArray ¶m, const QByteArray &actual, const QByteArray &value, QByteArray &uri, QString &oldValue, bool &error)
|
|
{
|
|
bool skipped = true;
|
|
if (error || actual != param) {
|
|
return skipped;
|
|
}
|
|
|
|
uri.remove(0, value.size());
|
|
skipped = false;
|
|
|
|
if (!oldValue.isNull()) {
|
|
qCDebug(logger) << "Found duplicate parameter" << param;
|
|
error = true;
|
|
return skipped;
|
|
}
|
|
|
|
const auto result = decodePercentEncoding(value);
|
|
if (!result) {
|
|
qCDebug(logger) << "Failed to decode" << param << "Invalid URI encoding or malformed UTF-8";
|
|
error = true;
|
|
return skipped;
|
|
}
|
|
|
|
oldValue = *result;
|
|
return skipped;
|
|
}
|
|
|
|
std::optional<QrParts> QrParts::parse(const QByteArray &qrCode)
|
|
{
|
|
static const QByteArray schemePrefix("otpauth://");
|
|
static const QByteArray totpType("totp");
|
|
static const QByteArray hotpType("hotp");
|
|
static const QByteArray issuerParam("issuer");
|
|
static const QByteArray secretParam("secret");
|
|
static const QByteArray algorithmParam("algorithm");
|
|
static const QByteArray tokenLengthParam("digits");
|
|
static const QByteArray timeStepParam("period");
|
|
static const QByteArray counterParam("counter");
|
|
|
|
QByteArray uri(qrCode);
|
|
if (!uri.startsWith(schemePrefix)) {
|
|
qCDebug(logger) << "Unexpected format: URI does not start with:" << schemePrefix;
|
|
return std::nullopt;
|
|
}
|
|
|
|
uri.remove(0, schemePrefix.size());
|
|
if (uri.size() < 4) {
|
|
qCDebug(logger) << "No token type found: URI too short";
|
|
return std::nullopt;
|
|
}
|
|
|
|
QByteArray typeField = uri.mid(0, 4);
|
|
if (typeField != totpType && typeField != hotpType) {
|
|
qCDebug(logger) << "Invalid token type found";
|
|
return std::nullopt;
|
|
}
|
|
|
|
Type type = typeField == totpType ? Type::Totp : Type::Hotp;
|
|
|
|
uri.remove(0, 4);
|
|
int paramOffset = uri.indexOf('?');
|
|
|
|
if (paramOffset < 0) {
|
|
qCDebug(logger) << "No token parameters found: URI too short";
|
|
return std::nullopt;
|
|
}
|
|
|
|
QString issuer;
|
|
QString name(QLatin1String(""));
|
|
|
|
if (uri[0] == '/') {
|
|
QByteArray issuerNameField = uri.mid(1, paramOffset - 1);
|
|
|
|
int colonOffset = issuerNameField.indexOf(':');
|
|
int encodedColonOffset = issuerNameField.indexOf(QByteArray("%3A"));
|
|
|
|
QByteArray issuerField;
|
|
QByteArray nameField = issuerNameField;
|
|
|
|
if (colonOffset >= 0 || encodedColonOffset >= 0) {
|
|
if (colonOffset < encodedColonOffset || encodedColonOffset < 0) {
|
|
issuerField = issuerNameField.mid(0, colonOffset);
|
|
nameField = issuerNameField.mid(colonOffset + 1);
|
|
} else {
|
|
issuerField = issuerNameField.mid(0, encodedColonOffset);
|
|
nameField = issuerNameField.mid(encodedColonOffset + 3);
|
|
}
|
|
|
|
const auto decodedIssuer = uri::decodePercentEncoding(issuerField);
|
|
if (!decodedIssuer) {
|
|
qCDebug(logger) << "Failed to decode issuer: invalid URI encoding or malformed UTF-8";
|
|
return std::nullopt;
|
|
}
|
|
|
|
issuer = *decodedIssuer;
|
|
}
|
|
|
|
const auto decodedName = uri::decodePercentEncoding(nameField);
|
|
if (!decodedName) {
|
|
qCDebug(logger) << "Failed to decode name: invalid URI encoding or malformed UTF-8";
|
|
return std::nullopt;
|
|
}
|
|
|
|
name = *decodedName;
|
|
|
|
uri.remove(0, paramOffset);
|
|
}
|
|
|
|
if (uri[0] != '?') {
|
|
qCDebug(logger) << "No token parameters found: expected to find:" << '?';
|
|
return std::nullopt;
|
|
}
|
|
|
|
QString secret;
|
|
QString counter;
|
|
QString timeStep;
|
|
QString algorithm;
|
|
QString tokenLength;
|
|
QString otherIssuer;
|
|
while (uri.size() > 1) {
|
|
uri.remove(0, 1);
|
|
QByteArray param;
|
|
int valueOffset = uri.indexOf('=');
|
|
switch (valueOffset) {
|
|
case -1:
|
|
qCDebug(logger) << "No parameter value found: URI too short";
|
|
return std::nullopt;
|
|
case 0:
|
|
qCDebug(logger) << "Found a parameter value without a name";
|
|
return std::nullopt;
|
|
default:
|
|
param = uri.mid(0, valueOffset);
|
|
uri.remove(0, valueOffset + 1);
|
|
break;
|
|
}
|
|
|
|
bool error = false;
|
|
int nextKeyOffset = uri.indexOf('&');
|
|
QByteArray value = uri.mid(0, nextKeyOffset);
|
|
if (tryDecodeParam(secretParam, param, value, uri, secret, error) &&
|
|
tryDecodeParam(issuerParam, param, value, uri, otherIssuer, error) &&
|
|
tryDecodeParam(tokenLengthParam, param, value, uri, tokenLength, error) &&
|
|
tryDecodeParam(timeStepParam, param, value, uri, timeStep, error) &&
|
|
tryDecodeParam(counterParam, param, value, uri, counter, error) &&
|
|
tryDecodeParam(algorithmParam, param, value, uri, algorithm, error)) {
|
|
qCDebug(logger) << "Invalid/unsupported parameter found";
|
|
return std::nullopt;
|
|
}
|
|
|
|
if (error) {
|
|
return std::nullopt;
|
|
}
|
|
}
|
|
|
|
if (secret.isEmpty()) {
|
|
qCDebug(logger) << "No token secret found: expected to find:" << *secretParam << "parameter";
|
|
return std::nullopt;
|
|
}
|
|
|
|
return std::optional<QrParts>(QrParts(
|
|
type,
|
|
name,
|
|
issuer.isNull() || (issuer.isEmpty() && !otherIssuer.isEmpty()) ? otherIssuer : issuer,
|
|
secret,
|
|
tokenLength,
|
|
counter,
|
|
timeStep,
|
|
algorithm
|
|
));
|
|
}
|
|
|
|
std::optional<QrParts> QrParts::parse(const QString &qrCode)
|
|
{
|
|
return parse(qrCode.toUtf8());
|
|
}
|
|
|
|
QrParts::Type QrParts::type(void) const
|
|
{
|
|
return m_type;
|
|
}
|
|
|
|
QString QrParts::algorithm(void) const
|
|
{
|
|
return m_algorithm;
|
|
}
|
|
|
|
QString QrParts::timeStep(void) const
|
|
{
|
|
return m_timeStep;
|
|
}
|
|
|
|
QString QrParts::tokenLength(void) const
|
|
{
|
|
return m_tokenLength;
|
|
}
|
|
|
|
QString QrParts::counter(void) const
|
|
{
|
|
return m_counter;
|
|
}
|
|
|
|
QString QrParts::secret(void) const
|
|
{
|
|
return m_secret;
|
|
}
|
|
|
|
QString QrParts::name(void) const
|
|
{
|
|
return m_name;
|
|
}
|
|
|
|
QString QrParts::issuer(void) const
|
|
{
|
|
return m_issuer;
|
|
}
|
|
|
|
QrParts::QrParts(Type type, const QString &name, const QString &issuer, const QString &secret,
|
|
const QString &tokenLength, const QString &counter, const QString &timeStep,
|
|
const QString &algorithm) : //, const Warnings &warnings) :
|
|
m_type(type), m_name(name), m_issuer(issuer), m_secret(secret), m_tokenLength(tokenLength),
|
|
m_counter(counter), m_timeStep(timeStep), m_algorithm(algorithm) //, m_warnings(warnings)
|
|
{
|
|
}
|
|
}
|