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: #14
master
Johan Ouwerkerk 2020-08-14 19:48:37 +02:00
parent 98f73c57a5
commit db51ce9e3f
8 changed files with 653 additions and 0 deletions

View File

@ -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)

View File

@ -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-)

View File

@ -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"

View File

@ -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"

View File

@ -7,6 +7,7 @@
#
add_subdirectory(base32)
add_subdirectory(uri)
add_subdirectory(hmac)
add_subdirectory(oath)
add_subdirectory(secrets)

10
src/uri/CMakeLists.txt Normal file
View File

@ -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)

282
src/uri/uri.cpp Normal file
View File

@ -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 &param, 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)
{
}
}

56
src/uri/uri.h Normal file
View File

@ -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