diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt index 27cc850..7d8a7b6 100644 --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -7,6 +7,7 @@ include_directories(BEFORE ../src) add_subdirectory(test-utils) add_subdirectory(base32) +add_subdirectory(uri) add_subdirectory(hmac) add_subdirectory(oath) add_subdirectory(secrets) diff --git a/autotests/uri/CMakeLists.txt b/autotests/uri/CMakeLists.txt new file mode 100644 index 0000000..a7a5588 --- /dev/null +++ b/autotests/uri/CMakeLists.txt @@ -0,0 +1,13 @@ +# +# SPDX-License-Identifier: BSD-2-Clause +# SPDX-FileCopyrightText: 2020 Johan Ouwerkerk +# + +set(Test_DEP_LIBS Qt5::Core Qt5::Test uri_lib) + +set(uri_test_SRCS + percent-encoding.cpp + qr-parsing.cpp +) + +ecm_add_tests(${uri_test_SRCS} LINK_LIBRARIES ${Test_DEP_LIBS} NAME_PREFIX uri-) diff --git a/autotests/uri/percent-encoding.cpp b/autotests/uri/percent-encoding.cpp new file mode 100644 index 0000000..182e297 --- /dev/null +++ b/autotests/uri/percent-encoding.cpp @@ -0,0 +1,122 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2020 Johan Ouwerkerk + */ +#include "uri/uri.h" + +#include +#include +#include + +class PercentEncodingTest: public QObject // clazy:exclude=ctor-missing-parent-argument +{ + Q_OBJECT +private Q_SLOTS: + void testValidString(void); + void testValidString_data(void); + void testValidByteArray(void); + void testValidByteArray_data(void); + void testInvalidString(void); + void testInvalidString_data(void); + void testInvalidByteArray(void); + void testInvalidByteArray_data(void); +}; + +void PercentEncodingTest::testValidString(void) +{ + QFETCH(QByteArray, input); + const std::optional result = uri::decodePercentEncoding(input); + QVERIFY2(result, "should decode valid input successfully"); + QTEST(*result, "expected"); +} + +void PercentEncodingTest::testValidString_data(void) +{ + QTest::addColumn("input"); + QTest::addColumn("expected"); + QVector validStringInputs = QVector() << QByteArray("%3A"); + QStringList validStringOutputs = QStringList() << QStringLiteral(":"); + int i = 0; + for (const auto &input : qAsConst(validStringInputs)) { + QTest::newRow(qPrintable(input)) << input << validStringOutputs[i]; + i++; + } +} + +void PercentEncodingTest::testValidByteArray(void) +{ + QFETCH(QByteArray, input); + const std::optional result = uri::fromPercentEncoding(input); + QVERIFY2(result, "should decode valid input successfully"); + QTEST(*result, "expected"); +} + +void PercentEncodingTest::testValidByteArray_data(void) +{ + QTest::addColumn("input"); + QTest::addColumn("expected"); + + QVector validByteArrayInputs = QVector() + << QByteArray("%01") + << QByteArray("%3A") + << QByteArray("%00") + << QByteArray("a%20valid%20sample") + << QByteArray("%2f") + << QByteArray("embedded%00works"); + + QVector validByteArrayOutputs = QVector() + << QByteArray("\x1") + << QByteArray(":") + << QByteArray("Z").replace('Z', '\0') + << QByteArray("a valid sample") + << QByteArray("/") + << QByteArray("embeddedZworks").replace('Z', '\0'); + + int i = 0; + for (const auto &input : qAsConst(validByteArrayInputs)) { + QTest::newRow(input.constData()) << input << validByteArrayOutputs[i]; + i++; + } +} + +void PercentEncodingTest::testInvalidString(void) +{ + QFETCH(QByteArray, input); + QVERIFY2(!uri::decodePercentEncoding(input), "should reject invalid input"); +} + +void PercentEncodingTest::testInvalidString_data(void) +{ + QTest::addColumn("input"); + QVector invalidStringInputs = QVector() + << QByteArray("%ff broken multibyte (no 0 in leading char)") + << QByteArray("%cf broken multibyte (next char not marked)") + << QByteArray("%c0%7f broken multibyte (over long)") + << QByteArray("truncated multibyte %c0"); + for (const auto &input : qAsConst(invalidStringInputs)) { + QTest::newRow(qPrintable(input)) << input; + } +} + +void PercentEncodingTest::testInvalidByteArray(void) +{ + QFETCH(QByteArray, input); + QVERIFY2(!uri::fromPercentEncoding(input), "should reject invalid input"); +} + +void PercentEncodingTest::testInvalidByteArray_data(void) +{ + QTest::addColumn("input"); + QVector invalidByteArrayInputs = QVector() + << QByteArray("%") + << QByteArray("invalid%") + << QByteArray("%G5") + << QByteArray("%5"); + for (const auto &input : qAsConst(invalidByteArrayInputs)) { + QTest::newRow(input.constData()) << input; + } +} + +QTEST_APPLESS_MAIN(PercentEncodingTest) + +#include "percent-encoding.moc" diff --git a/autotests/uri/qr-parsing.cpp b/autotests/uri/qr-parsing.cpp new file mode 100644 index 0000000..20a0b0f --- /dev/null +++ b/autotests/uri/qr-parsing.cpp @@ -0,0 +1,168 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2020 Johan Ouwerkerk + */ +#include "uri/uri.h" + +#include +#include + +Q_DECLARE_METATYPE(uri::QrParts::Type); + +class UriParsingTest: public QObject // clazy:exclude=ctor-missing-parent-argument +{ + Q_OBJECT +private Q_SLOTS: + void testValid(void); + void testValid_data(void); + void testInvalid(void); + void testInvalid_data(void); +}; + +static void define_valid_test_data(void) +{ + QTest::addColumn("input"); + QTest::addColumn("type"); + QTest::addColumn("name"); + QTest::addColumn("issuer"); + QTest::addColumn("secret"); + QTest::addColumn("timeStep"); + QTest::addColumn("tokenLength"); + QTest::addColumn("algorithm"); + QTest::addColumn("counter"); +} + +static void define_valid_test_case(const char *testCase, const QString &input, uri::QrParts::Type type, + const QString &name, const QString &issuer, const QString &secret, + const QString &timeStep, const QString &tokenLength, const QString &algorithm, + const QString &counter) +{ + QTest::newRow(qPrintable(testCase)) << input + << type << name << issuer << secret << timeStep << tokenLength << algorithm << counter; +} + +void UriParsingTest::testValid(void) +{ + QFETCH(QString, input); + + const std::optional result = uri::QrParts::parse(input); + QVERIFY2(result, "should parse valid input successfully"); + QTEST(result->type(), "type"); + QTEST(result->name(), "name"); + QTEST(result->issuer(), "issuer"); + QTEST(result->secret(), "secret"); + QTEST(result->tokenLength(), "tokenLength"); + QTEST(result->algorithm(), "algorithm"); + QTEST(result->counter(), "counter"); +} + +void UriParsingTest::testValid_data(void) +{ + define_valid_test_data(); + define_valid_test_case("hotp (all fields)", + QStringLiteral("otpauth://hotp/issuer:name?issuer=issuer&secret=VALUE&period=30&digits=6&algorithm=sha1&counter=42"), + uri::QrParts::Type::Hotp, QStringLiteral("name"), QStringLiteral("issuer"), + QStringLiteral("VALUE"), QStringLiteral("30"), QStringLiteral("6"), QStringLiteral("sha1"), + QStringLiteral("42")); + define_valid_test_case("hotp (all fields, URI encoded label separator)", + QStringLiteral("otpauth://hotp/issuer%3Ana:me?issuer=issuer&secret=VALUE&period=30&digits=6&algorithm=sha1&counter=42"), + uri::QrParts::Type::Hotp, QStringLiteral("na:me"), QStringLiteral("issuer"), + QStringLiteral("VALUE"), QStringLiteral("30"), QStringLiteral("6"), QStringLiteral("sha1"), + QStringLiteral("42")); + define_valid_test_case("hotp (minimal)", + QStringLiteral("otpauth://hotp/name?secret=VALUE&counter=42"), + uri::QrParts::Type::Hotp, QStringLiteral("name"), QString(), QStringLiteral("VALUE"), + QString(), QString(), QString(), QStringLiteral("42")); + define_valid_test_case("hotp (minimal, without name and missing params)", + QStringLiteral("otpauth://hotp?secret=VALUE"), + uri::QrParts::Type::Hotp, QString(), QString(), QStringLiteral("VALUE"), QString(), + QString(), QString(), QString()); + define_valid_test_case("hotp (issuer only in label)", + QStringLiteral("otpauth://hotp/issuer:name?secret=VALUE&counter=42"), + uri::QrParts::Type::Hotp, QStringLiteral("name"), QStringLiteral("issuer"), + QStringLiteral("VALUE"), QString(), QString(), QString(), QStringLiteral("42")); + define_valid_test_case("hotp (issuer only as param)", + QStringLiteral("otpauth://hotp/name?secret=VALUE&counter=42&issuer=issuer"), + uri::QrParts::Type::Hotp, QStringLiteral("name"), QStringLiteral("issuer"), + QStringLiteral("VALUE"), QString(), QString(), QString(), QStringLiteral("42")); + define_valid_test_case("hotp (inconsistent issuer)", + QStringLiteral("otpauth://hotp/issuer:name?secret=VALUE&counter=42&issuer=other"), + uri::QrParts::Type::Hotp, QStringLiteral("name"), QStringLiteral("issuer"), + QStringLiteral("VALUE"), QString(), QString(), QString(), QStringLiteral("42")); + define_valid_test_case("hotp (empty counter)", + QStringLiteral("otpauth://hotp/issuer:name?issuer=issuer&secret=VALUE&period=30&digits=6&algorithm=sha1&counter="), + uri::QrParts::Type::Hotp, QStringLiteral("name"), QStringLiteral("issuer"), + QStringLiteral("VALUE"), QStringLiteral("30"), QStringLiteral("6"), QStringLiteral("sha1"), + QString()); + define_valid_test_case("hotp (missing counter)", + QStringLiteral("otpauth://hotp/issuer:name?issuer=issuer&secret=VALUE&period=30&digits=6&algorithm=sha1"), + uri::QrParts::Type::Hotp, QStringLiteral("name"), QStringLiteral("issuer"), + QStringLiteral("VALUE"), QStringLiteral("30"), QStringLiteral("6"), QStringLiteral("sha1"), + QString()); + define_valid_test_case("totp (all fields, including redundant counter)", + QStringLiteral("otpauth://totp/issuer:name?issuer=issuer&secret=VALUE&period=30&digits=6&algorithm=sha1&counter=42"), + uri::QrParts::Type::Totp, QStringLiteral("name"), QStringLiteral("issuer"), + QStringLiteral("VALUE"), QStringLiteral("30"), QStringLiteral("6"), QStringLiteral("sha1"), + QStringLiteral("42")); + define_valid_test_case("totp (all fields, except redundant counter)", + QStringLiteral("otpauth://totp/issuer:name?issuer=issuer&secret=VALUE&period=30&digits=6&algorithm=sha1"), + uri::QrParts::Type::Totp, QStringLiteral("name"), QStringLiteral("issuer"), + QStringLiteral("VALUE"), QStringLiteral("30"), QStringLiteral("6"), QStringLiteral("sha1"), + QString()); + define_valid_test_case("totp (minimal)", + QStringLiteral("otpauth://totp/name?secret=VALUE"), + uri::QrParts::Type::Totp, QStringLiteral("name"), QString(), QStringLiteral("VALUE"), QString(), + QString(), QString(), QString()); + define_valid_test_case("totp (minimal, without name)", + QStringLiteral("otpauth://totp?secret=VALUE"), + uri::QrParts::Type::Totp, QString(), QString(), QStringLiteral("VALUE"), QString(), + QString(), QString(), QString()); + define_valid_test_case("hotp (with padding in secret)", + QStringLiteral("otpauth://hotp?secret=VALUE==="), + uri::QrParts::Type::Hotp, QString(), QString(), QStringLiteral("VALUE==="), QString(), + QString(), QString(), QString()); + define_valid_test_case("totp (with padding in secret)", + QStringLiteral("otpauth://totp?secret=VALUE===&period=30"), + uri::QrParts::Type::Totp, QString(), QString(), QStringLiteral("VALUE==="), + QStringLiteral("30"), QString(), QString(), QString()); +} + +void UriParsingTest::testInvalid(void) +{ + QFETCH(QString, input); + QVERIFY2(!uri::QrParts::parse(input), "should reject invalid input"); +} + +void UriParsingTest::testInvalid_data(void) +{ + QTest::addColumn("input"); + + QTest::newRow("wrong scheme") << QStringLiteral("http://localhost"); + QTest::newRow("missing type") << QStringLiteral("otpauth:///issuer:name?issuer=issuer&secret=VALUE&period=30&digits=6&algorithm=sha1&counter=42"); + QTest::newRow("unsupported type") << QStringLiteral("otpauth://wrong/issuer:name?issuer=issuer&secret=VALUE&period=30&digits=6&algorithm=sha1&counter=42"); + QTest::newRow("invalid param") << QStringLiteral("otpauth://hotp/issuer:name?issuer=issuer&secret=VALUE&period=30&digits=6&algorithm=sha1&counter=42&foo=bar"); + QTest::newRow("missing secret") << QStringLiteral("otpauth://hotp/issuer:name?issuer=issuer&period=30&digits=6&algorithm=sha1&counter=42"); + QTest::newRow("empty secret") << QStringLiteral("otpauth://hotp/issuer:name?issuer=issuer&secret=&period=30&digits=6&algorithm=sha1&counter=42"); + QTest::newRow("secret = bad utf8") << QStringLiteral("otpauth://hotp/issuer:name?issuer=issuer&secret=%c0%7fALUE&period=30&digits=6&algorithm=sha1&counter=42"); + QTest::newRow("secret = invalid percent encoding") << QStringLiteral("otpauth://hotp/issuer:name?issuer=issuer&secret=%VALUE&period=30&digits=6&algorithm=sha1&counter=42"); + QTest::newRow("counter = bad utf8") << QStringLiteral("otpauth://hotp/issuer:name?issuer=issuer&secret=VALUE&period=30&digits=6&algorithm=sha1&counter=%fc%80%80%80%80%80%80"); + QTest::newRow("counter = invalid percent encoding") << QStringLiteral("otpauth://hotp/issuer:name?issuer=issuer&secret=VALUE&period=30&digits=6&algorithm=sha1&counter=42%"); + QTest::newRow("name label = bad utf8") << QStringLiteral("otpauth://hotp/issuer:name%c1%00?issuer=issuer&secret=VALUE&period=30&digits=6&algorithm=sha1&counter=42"); + QTest::newRow("name label = invalid percent encoding") << QStringLiteral("otpauth://hotp/issuer:n%#ame?issuer=issuer&secret=VALUE&period=30&digits=6&algorithm=sha1&counter=42"); + QTest::newRow("issuer label = bad utf8") << QStringLiteral("otpauth://hotp/issuer%90:name?issuer=issuer&secret=VALUE&period=30&digits=6&algorithm=sha1&counter=42"); + QTest::newRow("issuer label = invalid percent encoding") << QStringLiteral("otpauth://hotp/is%suer:name?issuer=issuer&secret=VALUE&period=30&digits=6&algorithm=sha1&counter=42"); + QTest::newRow("issuer param = bad utf8") << QStringLiteral("otpauth://totp/issuer:name?issuer=issuer%cf&secret=VALUE&period=30&digits=%80&algorithm=sha1"); + QTest::newRow("issuer param = invalid percent encoding") << QStringLiteral("otpauth://totp/issuer:name?issuer=issu%er&secret=VALUE&period=30&digits=%S&algorithm=sha1"); + QTest::newRow("digits = bad utf8") << QStringLiteral("otpauth://totp/issuer:name?issuer=issuer&secret=VALUE&period=30&digits=%80&algorithm=sha1"); + QTest::newRow("digits = invalid percent encoding") << QStringLiteral("otpauth://totp/issuer:name?issuer=issuer&secret=VALUE&period=30&digits=%S&algorithm=sha1"); + QTest::newRow("period = bad utf8") << QStringLiteral("otpauth://totp/issuer:name?issuer=issuer&secret=VALUE&period=%c0&digits=6&algorithm=sha1"); + QTest::newRow("period = invalid percent encoding") << QStringLiteral("otpauth://totp/issuer:name?issuer=issuer&secret=VALUE&period=3%&digits=6&algorithm=sha1"); + QTest::newRow("algorithm = bad utf8") << QStringLiteral("otpauth://totp/issuer:name?issuer=issuer&secret=VALUE&period=30&digits=6&algorithm=sha%ff"); + QTest::newRow("algorithm = invalid percent encoding") << QStringLiteral("otpauth://totp/issuer:name?issuer=issuer&secret=VALUE&period=30&digits=6&algorithm=sha%1"); + QTest::newRow("hotp without params") << QStringLiteral("otpauth://hotp/issuer:name"); + QTest::newRow("totp without params") << QStringLiteral("otpauth://hotp/name"); +} + +QTEST_APPLESS_MAIN(UriParsingTest) + +#include "qr-parsing.moc" diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index cb03d90..7361e3f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -7,6 +7,7 @@ # add_subdirectory(base32) +add_subdirectory(uri) add_subdirectory(hmac) add_subdirectory(oath) add_subdirectory(secrets) diff --git a/src/uri/CMakeLists.txt b/src/uri/CMakeLists.txt new file mode 100644 index 0000000..1225949 --- /dev/null +++ b/src/uri/CMakeLists.txt @@ -0,0 +1,10 @@ +# +# SPDX-License-Identifier: BSD-2-Clause +# SPDX-FileCopyrightText: 2020 Johan Ouwerkerk +# +set(uri_SRCS + uri.cpp +) + +add_library(uri_lib STATIC ${uri_SRCS}) +target_link_libraries(uri_lib Qt5::Core) diff --git a/src/uri/uri.cpp b/src/uri/uri.cpp new file mode 100644 index 0000000..587b7aa --- /dev/null +++ b/src/uri/uri.cpp @@ -0,0 +1,282 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2020 Johan Ouwerkerk + */ +#include "uri.h" + +#include "../base32/base32.h" +#include "../logging_p.h" + +#include +#include +#include + +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 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(decoded); + } + + static std::optional 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(result) : std::nullopt; + } + + std::optional 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::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( + type, + name, + issuer.isNull() || (issuer.isEmpty() && !otherIssuer.isEmpty()) ? otherIssuer : issuer, + secret, + tokenLength, + counter, + timeStep, + algorithm + )); + } + + std::optional 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) + { + } +} diff --git a/src/uri/uri.h b/src/uri/uri.h new file mode 100644 index 0000000..a1f6932 --- /dev/null +++ b/src/uri/uri.h @@ -0,0 +1,56 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2020 Johan Ouwerkerk + */ +#ifndef OTPAUTH_URI_H +#define OTPAUTH_URI_H + +#include +#include +#include + +#include + +namespace uri +{ + /* + * A forgiving percent encoding "decoder" which does not get confused by invalid/malformed input + * (In contrast to QByteArray::fromPercentEncoding()). + */ + std::optional fromPercentEncoding(const QByteArray &encoded); + std::optional decodePercentEncoding(const QByteArray &utf8Data); + + class QrParts + { + public: + enum Type { + Totp, Hotp + }; + static std::optional parse(const QByteArray &qrCode); + static std::optional parse(const QString &qrCode); + public: + Type type(void) const; + QString algorithm(void) const; + QString timeStep(void) const; + QString tokenLength(void) const; + QString counter(void) const; + QString secret(void) const; + QString name(void) const; + QString issuer(void) const; + private: + explicit QrParts(Type type, const QString &name, const QString &issuer, const QString &secret, + const QString &tokenLength, const QString &counter, const QString &timeStep, + const QString &algorithm); + private: + const Type m_type; + const QString m_name; + const QString m_issuer; + const QString m_secret; + const QString m_tokenLength; + const QString m_counter; + const QString m_timeStep; + const QString m_algorithm; + }; +} + +#endif