feat: add basic support for otpauth:// URI parsing
This change provides a bare minimum implementation to parse an otpauth:// type URI into its component parts. Parsing is quite lax, and focused on what Keysmith can support or recover from in the intended UI/UX for adding accounts via QR codes. See-Also: https://github.com/google/google-authenticator/wiki/Key-Uri-Format Issues: #14master
parent
98f73c57a5
commit
db51ce9e3f
|
@ -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)
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
#
|
||||
# SPDX-License-Identifier: BSD-2-Clause
|
||||
# SPDX-FileCopyrightText: 2020 Johan Ouwerkerk <jm.ouwerkerk@gmail.com>
|
||||
#
|
||||
|
||||
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-)
|
|
@ -0,0 +1,122 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
* SPDX-FileCopyrightText: 2020 Johan Ouwerkerk <jm.ouwerkerk@gmail.com>
|
||||
*/
|
||||
#include "uri/uri.h"
|
||||
|
||||
#include <QTest>
|
||||
#include <QVector>
|
||||
#include <QtDebug>
|
||||
|
||||
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<QString> result = uri::decodePercentEncoding(input);
|
||||
QVERIFY2(result, "should decode valid input successfully");
|
||||
QTEST(*result, "expected");
|
||||
}
|
||||
|
||||
void PercentEncodingTest::testValidString_data(void)
|
||||
{
|
||||
QTest::addColumn<QByteArray>("input");
|
||||
QTest::addColumn<QString>("expected");
|
||||
QVector<QByteArray> validStringInputs = QVector<QByteArray>() << 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<QByteArray> result = uri::fromPercentEncoding(input);
|
||||
QVERIFY2(result, "should decode valid input successfully");
|
||||
QTEST(*result, "expected");
|
||||
}
|
||||
|
||||
void PercentEncodingTest::testValidByteArray_data(void)
|
||||
{
|
||||
QTest::addColumn<QByteArray>("input");
|
||||
QTest::addColumn<QByteArray>("expected");
|
||||
|
||||
QVector<QByteArray> validByteArrayInputs = QVector<QByteArray>()
|
||||
<< QByteArray("%01")
|
||||
<< QByteArray("%3A")
|
||||
<< QByteArray("%00")
|
||||
<< QByteArray("a%20valid%20sample")
|
||||
<< QByteArray("%2f")
|
||||
<< QByteArray("embedded%00works");
|
||||
|
||||
QVector<QByteArray> validByteArrayOutputs = QVector<QByteArray>()
|
||||
<< 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<QByteArray>("input");
|
||||
QVector<QByteArray> invalidStringInputs = QVector<QByteArray>()
|
||||
<< 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<QByteArray>("input");
|
||||
QVector<QByteArray> invalidByteArrayInputs = QVector<QByteArray>()
|
||||
<< 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"
|
|
@ -0,0 +1,168 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
* SPDX-FileCopyrightText: 2020 Johan Ouwerkerk <jm.ouwerkerk@gmail.com>
|
||||
*/
|
||||
#include "uri/uri.h"
|
||||
|
||||
#include <QTest>
|
||||
#include <QtDebug>
|
||||
|
||||
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<QString>("input");
|
||||
QTest::addColumn<uri::QrParts::Type>("type");
|
||||
QTest::addColumn<QString>("name");
|
||||
QTest::addColumn<QString>("issuer");
|
||||
QTest::addColumn<QString>("secret");
|
||||
QTest::addColumn<QString>("timeStep");
|
||||
QTest::addColumn<QString>("tokenLength");
|
||||
QTest::addColumn<QString>("algorithm");
|
||||
QTest::addColumn<QString>("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<uri::QrParts> 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<QString>("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"
|
|
@ -7,6 +7,7 @@
|
|||
#
|
||||
|
||||
add_subdirectory(base32)
|
||||
add_subdirectory(uri)
|
||||
add_subdirectory(hmac)
|
||||
add_subdirectory(oath)
|
||||
add_subdirectory(secrets)
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
#
|
||||
# SPDX-License-Identifier: BSD-2-Clause
|
||||
# SPDX-FileCopyrightText: 2020 Johan Ouwerkerk <jm.ouwerkerk@gmail.com>
|
||||
#
|
||||
set(uri_SRCS
|
||||
uri.cpp
|
||||
)
|
||||
|
||||
add_library(uri_lib STATIC ${uri_SRCS})
|
||||
target_link_libraries(uri_lib Qt5::Core)
|
|
@ -0,0 +1,282 @@
|
|||
/*
|
||||
* 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)
|
||||
{
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
* SPDX-FileCopyrightText: 2020 Johan Ouwerkerk <jm.ouwerkerk@gmail.com>
|
||||
*/
|
||||
#ifndef OTPAUTH_URI_H
|
||||
#define OTPAUTH_URI_H
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
#include <optional>
|
||||
|
||||
namespace uri
|
||||
{
|
||||
/*
|
||||
* A forgiving percent encoding "decoder" which does not get confused by invalid/malformed input
|
||||
* (In contrast to QByteArray::fromPercentEncoding()).
|
||||
*/
|
||||
std::optional<QByteArray> fromPercentEncoding(const QByteArray &encoded);
|
||||
std::optional<QString> decodePercentEncoding(const QByteArray &utf8Data);
|
||||
|
||||
class QrParts
|
||||
{
|
||||
public:
|
||||
enum Type {
|
||||
Totp, Hotp
|
||||
};
|
||||
static std::optional<QrParts> parse(const QByteArray &qrCode);
|
||||
static std::optional<QrParts> 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
|
Loading…
Reference in New Issue