Port base32 decoding away from liboath
Provide a custom base32 implementation; relates to issues: #9 and #6. In particular being able to control memory allocation prior to decoding base32 will help with resolving issue #6 in a (more) secure fashion.master
parent
bf5dba5b58
commit
ea81dafb8e
|
@ -1,6 +1,14 @@
|
|||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
# SPDX-FileCopyrightText: 2019-2020 Johan Ouwerkerk <jm.ouwerkerk@gmail.com>
|
||||
#
|
||||
|
||||
set(Test_DEP_LIBS Qt5::Core Qt5::Test ${LIBOATH_LIBRARIES} base32_lib)
|
||||
set(base32_lib_test_SRCS base32-decode.cpp)
|
||||
set(Test_DEP_LIBS Qt5::Core Qt5::Test base32_lib)
|
||||
set(base32_lib_test_SRCS
|
||||
base32-decode.cpp
|
||||
base32-coding-decoding.cpp
|
||||
base32-validate.cpp
|
||||
)
|
||||
|
||||
ecm_add_tests(
|
||||
${base32_lib_test_SRCS}
|
||||
|
|
|
@ -0,0 +1,130 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
* SPDX-FileCopyrightText: 2020 Johan Ouwerkerk <jm.ouwerkerk@gmail.com>
|
||||
*/
|
||||
#include "base32/base32.h"
|
||||
|
||||
#include <QTest>
|
||||
#include <QtDebug>
|
||||
|
||||
class Base32CodingDecodingTest: public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
private Q_SLOTS:
|
||||
void testSample(void);
|
||||
void testSample_data(void);
|
||||
};
|
||||
|
||||
static int lastPadBits(int data)
|
||||
{
|
||||
switch (data) {
|
||||
case 7:
|
||||
return 3;
|
||||
case 5:
|
||||
return 1;
|
||||
case 4:
|
||||
return 4;
|
||||
case 2:
|
||||
return 2;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static int outputSize(int data)
|
||||
{
|
||||
switch (data) {
|
||||
case 8:
|
||||
return 5;
|
||||
case 7:
|
||||
return 4;
|
||||
case 5:
|
||||
return 3;
|
||||
case 4:
|
||||
return 2;
|
||||
case 2:
|
||||
return 1;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static void define_test_data(void)
|
||||
{
|
||||
QTest::addColumn<QString>("input");
|
||||
QTest::addColumn<QByteArray>("expected");
|
||||
}
|
||||
|
||||
static void define_test_case(const QString &input, int len, char value)
|
||||
{
|
||||
static const QString testCase(QLatin1String("size: %1: '%2' ... 0x%3"));
|
||||
QByteArray expected;
|
||||
int outputSz = outputSize(len);
|
||||
expected.reserve(outputSz);
|
||||
expected.resize(outputSz);
|
||||
expected.fill('\x0');
|
||||
if (len > 0) {
|
||||
expected[expected.size() - 1] = value;
|
||||
}
|
||||
|
||||
QTest::newRow(qPrintable(testCase.arg(len).arg(input).arg(QLatin1String(expected.toHex())))) << input << expected;
|
||||
}
|
||||
|
||||
static inline QChar pick(int v)
|
||||
{
|
||||
return v < 26 ? QLatin1Char('A' + v) : QLatin1Char('2' + v - 26);
|
||||
}
|
||||
|
||||
static void define_test_case(int len)
|
||||
{
|
||||
QString prefix;
|
||||
QByteArray output;
|
||||
int padBits = lastPadBits(len);
|
||||
for(int i = 3; i < len; ++i) {
|
||||
prefix += QLatin1Char('A');
|
||||
}
|
||||
|
||||
for (int b = 0; b < 256; ++b) {
|
||||
int i1 = ((b << padBits) >> 10) & 0x1F;
|
||||
int i2 = (b >> (5 - padBits)) & 0x1F;
|
||||
int i3 = (b << padBits) & 0x1F;
|
||||
|
||||
QString input = prefix;
|
||||
if (len >= 3) {
|
||||
input += pick(i1);
|
||||
}
|
||||
input += pick(i2);
|
||||
input += pick(i3);
|
||||
|
||||
while(input.size() < 8) {
|
||||
input += QLatin1Char('=');
|
||||
}
|
||||
define_test_case(input, len, b);
|
||||
}
|
||||
}
|
||||
|
||||
void Base32CodingDecodingTest::testSample(void)
|
||||
{
|
||||
QFETCH(QString, input);
|
||||
QFETCH(QByteArray, expected);
|
||||
|
||||
QByteArray work(expected.size(), '\x0');
|
||||
|
||||
QCOMPARE(base32::decode(input, work.data(), work.size()), std::optional<size_t>(expected.size()));
|
||||
QCOMPARE(work, expected);
|
||||
}
|
||||
|
||||
void Base32CodingDecodingTest::testSample_data(void)
|
||||
{
|
||||
define_test_data();
|
||||
QTest::newRow(qPrintable(QLatin1String("the empty string"))) << QString(QLatin1String("")) << QByteArray();
|
||||
define_test_case(2);
|
||||
define_test_case(4);
|
||||
define_test_case(5);
|
||||
define_test_case(7);
|
||||
define_test_case(8);
|
||||
}
|
||||
|
||||
QTEST_APPLESS_MAIN(Base32CodingDecodingTest)
|
||||
|
||||
#include "base32-coding-decoding.moc"
|
|
@ -1,50 +1,62 @@
|
|||
/*****************************************************************************
|
||||
* Copyright: 2019 Johan Ouwerkerk <jm.ouwerkerk@gmail.com> *
|
||||
* *
|
||||
* This project is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This project is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
****************************************************************************/
|
||||
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
* SPDX-FileCopyrightText: 2019-2020 Johan Ouwerkerk <jm.ouwerkerk@gmail.com>
|
||||
*/
|
||||
#include "base32/base32.h"
|
||||
|
||||
#include <QTest>
|
||||
#include <QtDebug>
|
||||
|
||||
Q_DECLARE_METATYPE(std::optional<QByteArray>);
|
||||
|
||||
class Base32DecodingTest: public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
private Q_SLOTS:
|
||||
void testSample(void);
|
||||
void testSample_data(void);
|
||||
void testValidSample(void);
|
||||
void testValidSample_data(void);
|
||||
void testInvalidSample(void);
|
||||
void testInvalidSample_data(void);
|
||||
};
|
||||
|
||||
static void define_test_case(const char *testCase, const char *data, int length, const QString &base32)
|
||||
static void define_valid_test_case(const char *testCase, const char *data, int length, const QString &base32)
|
||||
{
|
||||
std::optional<QByteArray> msg = data ? std::optional<QByteArray>(QByteArray(data, length)) : std::nullopt;
|
||||
QTest::newRow(qPrintable(QLatin1String(testCase))) << msg << base32;
|
||||
QTest::newRow(qPrintable(QLatin1String(testCase))) << base32 << QByteArray(data, length);
|
||||
}
|
||||
|
||||
void Base32DecodingTest::testSample(void)
|
||||
static void define_invalid_test_case(const char *testCase, const QString &base32)
|
||||
{
|
||||
QTest::newRow(qPrintable(QLatin1String(testCase))) << base32;
|
||||
}
|
||||
|
||||
void Base32DecodingTest::testValidSample(void)
|
||||
{
|
||||
QFETCH(QString, base32);
|
||||
QTEST(base32::decode(base32), "message");
|
||||
QFETCH(QByteArray, expected);
|
||||
|
||||
QByteArray work;
|
||||
work.reserve(expected.size());
|
||||
work.resize(expected.size());
|
||||
work.fill('\x0');
|
||||
|
||||
QCOMPARE(base32::decode(base32, work.data(), work.size()), std::optional<size_t>(expected.size()));
|
||||
QCOMPARE(work, expected);
|
||||
|
||||
QCOMPARE(base32::decode(base32), std::optional<QByteArray>(expected));
|
||||
}
|
||||
|
||||
void Base32DecodingTest::testSample_data(void)
|
||||
void Base32DecodingTest::testInvalidSample(void)
|
||||
{
|
||||
QFETCH(QString, base32);
|
||||
|
||||
QByteArray work;
|
||||
work.reserve(100);
|
||||
work.resize(100);
|
||||
work.fill('\x0');
|
||||
|
||||
QCOMPARE(base32::decode(base32, work.data(), work.size()), std::nullopt);
|
||||
QCOMPARE(base32::decode(base32), std::nullopt);
|
||||
}
|
||||
|
||||
void Base32DecodingTest::testValidSample_data(void)
|
||||
{
|
||||
static const char ok_corpus[13][5] = {
|
||||
{ 'A', 'B', 'C', 'D', '\xA' },
|
||||
|
@ -66,35 +78,39 @@ void Base32DecodingTest::testSample_data(void)
|
|||
{}
|
||||
};
|
||||
|
||||
QTest::addColumn<std::optional<QByteArray>>("message");
|
||||
QTest::addColumn<QString>("base32");
|
||||
QTest::addColumn<QByteArray>("expected");
|
||||
|
||||
define_test_case("'ABCD\\n'", ok_corpus[0], 5, QLatin1String("IFBEGRAK"));
|
||||
define_test_case("'?ABCD'", ok_corpus[1], 5, QLatin1String("H5AUEQ2E"));
|
||||
define_test_case("'2016'", ok_corpus[2], 4, QLatin1String("GIYDCNQ="));
|
||||
define_test_case("'=='", ok_corpus[3], 2, QLatin1String("HU6Q===="));
|
||||
define_test_case("'?'", ok_corpus[4], 1, QLatin1String("H4======"));
|
||||
define_test_case("'8'", ok_corpus[5], 1, QLatin1String("HA======"));
|
||||
define_valid_test_case("'ABCD\\n'", ok_corpus[0], 5, QLatin1String("IFBEGRAK"));
|
||||
define_valid_test_case("'?ABCD'", ok_corpus[1], 5, QLatin1String("H5AUEQ2E"));
|
||||
define_valid_test_case("'2016'", ok_corpus[2], 4, QLatin1String("GIYDCNQ="));
|
||||
define_valid_test_case("'=='", ok_corpus[3], 2, QLatin1String("HU6Q===="));
|
||||
define_valid_test_case("'?'", ok_corpus[4], 1, QLatin1String("H4======"));
|
||||
define_valid_test_case("'8'", ok_corpus[5], 1, QLatin1String("HA======"));
|
||||
|
||||
define_test_case("'\\x0\\x1\\x2'", ok_corpus[6], 3, QLatin1String("AAAQE==="));
|
||||
define_test_case("'\\x1\\x0\\x2'", ok_corpus[7], 3, QLatin1String("AEAAE==="));
|
||||
define_test_case("'\\x1\\x2\\x0'", ok_corpus[8], 3, QLatin1String("AEBAA==="));
|
||||
define_valid_test_case("'\\x0\\x1\\x2'", ok_corpus[6], 3, QLatin1String("AAAQE==="));
|
||||
define_valid_test_case("'\\x1\\x0\\x2'", ok_corpus[7], 3, QLatin1String("AEAAE==="));
|
||||
define_valid_test_case("'\\x1\\x2\\x0'", ok_corpus[8], 3, QLatin1String("AEBAA==="));
|
||||
|
||||
define_test_case("'\\x0AB\\x1\\x2'", ok_corpus[9], 5, QLatin1String("ABAUEAIC"));
|
||||
define_test_case("'\\x1AB\\x0\\x2'", ok_corpus[10], 5, QLatin1String("AFAUEAAC"));
|
||||
define_test_case("'\\x1AB\\x2\\x0'", ok_corpus[11], 5, QLatin1String("AFAUEAQA"));
|
||||
define_valid_test_case("'\\x0AB\\x1\\x2'", ok_corpus[9], 5, QLatin1String("ABAUEAIC"));
|
||||
define_valid_test_case("'\\x1AB\\x0\\x2'", ok_corpus[10], 5, QLatin1String("AFAUEAAC"));
|
||||
define_valid_test_case("'\\x1AB\\x2\\x0'", ok_corpus[11], 5, QLatin1String("AFAUEAQA"));
|
||||
|
||||
define_test_case("''", ok_corpus[12], 0, QLatin1String(""));
|
||||
|
||||
define_test_case("without any padding", NULL, 0, QLatin1String("ZZ"));
|
||||
define_test_case("too little padding", NULL, 0, QLatin1String("ZZ==="));
|
||||
define_test_case("padding only", NULL, 0, QLatin1String("========"));
|
||||
define_test_case("embedded spaces", NULL, 0, QLatin1String("ZZ \n===="));
|
||||
define_test_case("invalid base32 (1)", NULL, 0, QLatin1String("1AABBCCD"));
|
||||
define_test_case("invalid base32 (8)", NULL, 0, QLatin1String("AABBCC8D"));
|
||||
define_test_case("invalid base32 (@)", NULL, 0, QLatin1String("AABBCCD@"));
|
||||
define_valid_test_case("''", ok_corpus[12], 0, QLatin1String(""));
|
||||
}
|
||||
|
||||
void Base32DecodingTest::testInvalidSample_data(void)
|
||||
{
|
||||
QTest::addColumn<QString>("base32");
|
||||
|
||||
define_invalid_test_case("without any padding", QLatin1String("ZZ"));
|
||||
define_invalid_test_case("too little padding", QLatin1String("ZZ==="));
|
||||
define_invalid_test_case("padding only", QLatin1String("========"));
|
||||
define_invalid_test_case("embedded spaces", QLatin1String("ZZ \n===="));
|
||||
define_invalid_test_case("invalid base32 (1)", QLatin1String("1AABBCCD"));
|
||||
define_invalid_test_case("invalid base32 (8)", QLatin1String("AABBCC8D"));
|
||||
define_invalid_test_case("invalid base32 (@)", QLatin1String("AABBCCD@"));
|
||||
}
|
||||
|
||||
QTEST_APPLESS_MAIN(Base32DecodingTest)
|
||||
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
* SPDX-FileCopyrightText: 2020 Johan Ouwerkerk <jm.ouwerkerk@gmail.com>
|
||||
*/
|
||||
#include "base32/base32.h"
|
||||
|
||||
#include <QTest>
|
||||
#include <QtDebug>
|
||||
|
||||
class Base32ValidationTest: public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
private Q_SLOTS:
|
||||
void testValidSample(void);
|
||||
void testValidSample_data(void);
|
||||
void testInvalidSample(void);
|
||||
void testInvalidSample_data(void);
|
||||
};
|
||||
|
||||
static void define_valid_test_case(const QString &base32, size_t expected)
|
||||
{
|
||||
static const QString testCase(QLatin1String("'%1'"));
|
||||
QTest::newRow(qPrintable(testCase.arg(base32))) << base32 << expected;
|
||||
}
|
||||
|
||||
static void define_invalid_test_case(const char *testCase, const QString &input)
|
||||
{
|
||||
QTest::newRow(qPrintable(testCase)) << input;
|
||||
}
|
||||
|
||||
void Base32ValidationTest::testValidSample(void)
|
||||
{
|
||||
QFETCH(QString, input);
|
||||
QFETCH(size_t, expected);
|
||||
QCOMPARE(base32::validate(input), std::optional<size_t>(expected));
|
||||
}
|
||||
|
||||
void Base32ValidationTest::testInvalidSample(void)
|
||||
{
|
||||
QFETCH(QString, input);
|
||||
QVERIFY2(!base32::validate(input), "invalid input should be rejected");
|
||||
}
|
||||
|
||||
void Base32ValidationTest::testValidSample_data(void)
|
||||
{
|
||||
QTest::addColumn<QString>("input");
|
||||
QTest::addColumn<size_t>("expected");
|
||||
|
||||
define_valid_test_case(QLatin1String("IFBEGRAK"), 5);
|
||||
define_valid_test_case(QLatin1String("GIYDCNQ="), 4);
|
||||
define_valid_test_case(QLatin1String("AAAQE==="), 3);
|
||||
define_valid_test_case(QLatin1String("HU6Q===="), 2);
|
||||
define_valid_test_case(QLatin1String("H4======"), 1);
|
||||
|
||||
define_valid_test_case(QLatin1String(""), 0);
|
||||
}
|
||||
|
||||
void Base32ValidationTest::testInvalidSample_data(void)
|
||||
{
|
||||
QTest::addColumn<QString>("input");
|
||||
|
||||
define_invalid_test_case("without any padding", QLatin1String("ZZ"));
|
||||
define_invalid_test_case("too little padding", QLatin1String("ZZ==="));
|
||||
define_invalid_test_case("padding only", QLatin1String("========"));
|
||||
define_invalid_test_case("embedded spaces", QLatin1String("ZZ \n===="));
|
||||
define_invalid_test_case("invalid base32 (1)", QLatin1String("1AABBCCD"));
|
||||
define_invalid_test_case("invalid base32 (8)", QLatin1String("AABBCC8D"));
|
||||
define_invalid_test_case("invalid base32 (@)", QLatin1String("AABBCCD@"));
|
||||
}
|
||||
|
||||
QTEST_APPLESS_MAIN(Base32ValidationTest)
|
||||
|
||||
#include "base32-validate.moc"
|
|
@ -1,8 +1,9 @@
|
|||
#
|
||||
# This directory contains a wrapper around base32 functionality of the oath library
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
# SPDX-FileCopyrightText: 2019 Johan Ouwerkerk <jm.ouwerkerk@gmail.com>
|
||||
# SPDX-FileCopyrightText: 2020 Johan Ouwerkerk <jm.ouwerkerk@gmail.com>
|
||||
#
|
||||
|
||||
set(base32_SRCS base32.cpp)
|
||||
|
||||
add_library(base32_lib STATIC ${base32_SRCS})
|
||||
target_link_libraries(base32_lib Qt5::Core ${LIBOATH_LIBRARIES})
|
||||
target_link_libraries(base32_lib Qt5::Core)
|
||||
|
|
|
@ -1,116 +1,282 @@
|
|||
/*****************************************************************************
|
||||
* Copyright: 2019 Johan Ouwerkerk <jm.ouwerkerk@gmail.com> *
|
||||
* *
|
||||
* This project is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This project is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
****************************************************************************/
|
||||
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
* SPDX-FileCopyrightText: 2019-2020 Johan Ouwerkerk <jm.ouwerkerk@gmail.com>
|
||||
*/
|
||||
#include "base32.h"
|
||||
|
||||
#include "../oath_p.h"
|
||||
static const QChar alphaMinLowerCase(QLatin1Char('a'));
|
||||
static const QChar alphaMaxLowerCase(QLatin1Char('z'));
|
||||
static const QChar alphaMinUpperCase(QLatin1Char('A'));
|
||||
static const QChar alphaMaxUpperCase(QLatin1Char('Z'));
|
||||
static const QChar numMin(QLatin1Char('2'));
|
||||
static const QChar numMax(QLatin1Char('7'));
|
||||
static const QChar pad(QLatin1Char('='));
|
||||
|
||||
#include <QtDebug>
|
||||
static inline bool checkInputRange(const QString &encoded, int from, int until)
|
||||
{
|
||||
/*
|
||||
* from should be between 0 (inclusive) and size (exclusive)
|
||||
* until should be between from (inclusive) and size (inclusive)
|
||||
* total range size (until - from) should be a multiple of 8 or it is not valid base32
|
||||
*/
|
||||
int size = encoded.size();
|
||||
return from >= 0 && from <= size && until >= from && until <= size && ((until - from) % 8) == 0;
|
||||
}
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
static std::optional<int> decode(const QChar &chr)
|
||||
{
|
||||
if (chr >= alphaMinLowerCase && chr <= alphaMaxLowerCase) {
|
||||
return std::optional<int>(chr.toLatin1() - alphaMinLowerCase.toLatin1());
|
||||
}
|
||||
if (chr >= alphaMinUpperCase && chr <= alphaMaxUpperCase) {
|
||||
return std::optional<int>(chr.toLatin1() - alphaMinUpperCase.toLatin1());
|
||||
}
|
||||
if (chr >= numMin && chr <= numMax) {
|
||||
return std::optional<int>(26 + chr.toLatin1() - numMin.toLatin1());
|
||||
}
|
||||
|
||||
if (chr >= pad) {
|
||||
return std::optional<int>(0);
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
static std::optional<quint64> decode(const QString &encoded, int index)
|
||||
{
|
||||
quint64 result = 0ULL;
|
||||
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
std::optional<int> v = decode(encoded[index + i]);
|
||||
if (v) {
|
||||
result = (result << 5) | *v;
|
||||
} else {
|
||||
// TODO warn about this
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
return std::optional<quint64>(result);
|
||||
}
|
||||
|
||||
static std::optional<size_t> decode(const QString &encoded, int index, int end, int padding, size_t offset, size_t capacity, char * const output)
|
||||
{
|
||||
Q_ASSERT_X(offset <= capacity, Q_FUNC_INFO, "invalid offset into output buffer");
|
||||
Q_ASSERT_X(end >= 0 && end <= encoded.size(), Q_FUNC_INFO, "end of encoded data should be valid");
|
||||
Q_ASSERT_X(padding >= 0 && padding <= end, Q_FUNC_INFO, "padding index should be valid");
|
||||
Q_ASSERT_X(index >= 0 && index <= padding && ((end - index) % 8) == 0, Q_FUNC_INFO, "index should be valid");
|
||||
|
||||
size_t group;
|
||||
|
||||
switch ((index + 8) - padding)
|
||||
{
|
||||
case 2:
|
||||
case 5:
|
||||
case 7:
|
||||
Q_ASSERT_X(false, Q_FUNC_INFO, "invalid amount of padding should have been caught by previous validation");
|
||||
return std::nullopt;
|
||||
case 1:
|
||||
group = 4;
|
||||
break;
|
||||
case 3:
|
||||
group = 3;
|
||||
break;
|
||||
case 4:
|
||||
group = 2;
|
||||
break;
|
||||
case 6:
|
||||
group = 1;
|
||||
break;
|
||||
default: // no padding (yet) for the group at the given index: there are 8 or more bytes left
|
||||
group = 5;
|
||||
break;
|
||||
}
|
||||
|
||||
Q_ASSERT_X((capacity - offset) >= group, Q_FUNC_INFO, "offset/output group too big for output buffer size");
|
||||
|
||||
std::optional<quint64> bits = decode(encoded, index);
|
||||
|
||||
Q_ASSERT_X(bits, Q_FUNC_INFO, "invalid input should have been caught by prior validation");
|
||||
|
||||
quint64 value = *bits;
|
||||
for (size_t i = 0; i < group; ++i) {
|
||||
output[offset + i] = (char) ((value >> (32ULL - i * 8ULL)) & 0xFFULL);
|
||||
}
|
||||
|
||||
return std::optional<size_t>(group);
|
||||
}
|
||||
|
||||
static inline bool isBase32(const QChar &c)
|
||||
{
|
||||
return (c >= alphaMinLowerCase && c <= alphaMaxLowerCase) || (c >= alphaMinUpperCase && c <= alphaMaxUpperCase) || (c >= numMin && c <= numMax);
|
||||
}
|
||||
|
||||
static bool isPaddingValid(const QString &encoded, int paddingIndex, int amount)
|
||||
{
|
||||
static const int padMasks[7] = {
|
||||
0x7, // 8 - 1 padding -> 7 * 5 - 32 bits -> 3 trailing bits: mask 0x7
|
||||
0x0, // 8 - 2 padding -> invalid
|
||||
0x1, // 8 - 3 padding -> 5 * 5 - 24 bits -> 1 trailing bit : mask 0x1
|
||||
0xF, // 8 - 4 padding -> 4 * 5 - 16 bits -> 4 trailing bits: mask 0xF
|
||||
0x0, // 8 - 5 padding -> invalid
|
||||
0x3, // 8 - 6 padding -> 2 * 5 - 8 bits -> 2 trailing bits: mask 0x3
|
||||
0x0 // 8 - 7 padding -> invalid
|
||||
};
|
||||
|
||||
if (amount == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (amount >= 8) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Q_ASSERT_X(paddingIndex >= 0, Q_FUNC_INFO, "invalid amount of padding should have been caught by previous validation");
|
||||
|
||||
const QChar c = encoded[paddingIndex - 1];
|
||||
Q_ASSERT_X(c != pad, Q_FUNC_INFO, "invalid amount of padding should have been caught by previous validation");
|
||||
|
||||
/*
|
||||
* Check if the amount of padding corresponds to a known (valid) input 'group' size
|
||||
* by looking up the mask for the last character before padding (0 = invalid)
|
||||
*/
|
||||
int p = padMasks[amount - 1];
|
||||
if (p == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::optional<int> d = decode(c);
|
||||
Q_ASSERT_X(d, Q_FUNC_INFO, "invalid input should have been caught by prior validation");
|
||||
|
||||
/*
|
||||
* check if there are no trailing bits,
|
||||
* i.e. the last character before padding does not encode bits that are not whitelisted by the mask
|
||||
*/
|
||||
return ((*d) & p) == 0;
|
||||
|
||||
}
|
||||
|
||||
static std::optional<int> isBase32(const QString &encoded, int from, int until)
|
||||
{
|
||||
if (!checkInputRange(encoded, from, until)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
int paddingIndex = until;
|
||||
for (int i = from; i < until; ++i) {
|
||||
const QChar at = encoded[i];
|
||||
if (at == pad) {
|
||||
if (paddingIndex == until) {
|
||||
paddingIndex = i;
|
||||
}
|
||||
} else {
|
||||
/*
|
||||
* Reject input if:
|
||||
* - padding has 'started' but the current character is not the padding character
|
||||
* - the current character is not a (valid) value character
|
||||
*/
|
||||
if (paddingIndex < until || !isBase32(at)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int amount = until - paddingIndex;
|
||||
|
||||
return isPaddingValid(encoded, paddingIndex, amount) ? std::optional<int>(paddingIndex) : std::nullopt;
|
||||
}
|
||||
|
||||
static inline size_t determineCapacity(size_t encodedBytes, size_t accountFor, size_t lastBytes)
|
||||
{
|
||||
return 5 * (encodedBytes - accountFor) / 8 + lastBytes;
|
||||
}
|
||||
|
||||
namespace base32
|
||||
static size_t requiredCapacity(int paddingIndex, int from, int until)
|
||||
{
|
||||
std::optional<QByteArray> decode(const QString &encoded)
|
||||
{
|
||||
QByteArray result;
|
||||
QByteArray bytes = encoded.toLocal8Bit();
|
||||
int size = bytes.size(), capacity = size;
|
||||
bool ok = false;
|
||||
|
||||
// size should be a multiple of 8 if the input is to be valid base32
|
||||
// (smaller data should be padded correctly)
|
||||
if (size % 8) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
while (size > 0 && bytes[size -1] == '=') {
|
||||
size --;
|
||||
}
|
||||
|
||||
// based on the amount of padding, determine the exact size of the encoded data
|
||||
switch (capacity - size) {
|
||||
case 0:
|
||||
capacity = determineCapacity(size, 0, 0);
|
||||
break;
|
||||
case 1:
|
||||
capacity = determineCapacity(size, 7, 4);
|
||||
break;
|
||||
case 3:
|
||||
capacity = determineCapacity(size, 5, 3);
|
||||
break;
|
||||
case 4:
|
||||
capacity = determineCapacity(size, 4, 2);
|
||||
break;
|
||||
case 6:
|
||||
capacity = determineCapacity(size, 2, 1);
|
||||
break;
|
||||
default:
|
||||
// invalid amount of padding, reject the input
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/*
|
||||
* We want to fill this buffer *exactly* if possible, to avoid accidental copies of (partial) secrets
|
||||
* when filling in the decoded secret later
|
||||
*/
|
||||
result.reserve(capacity);
|
||||
|
||||
char * output = nullptr;
|
||||
size_t reportedCapacity = (size_t) capacity;
|
||||
|
||||
int status = oath_base32_decode(bytes.data(), (size_t) size, &output, &reportedCapacity);
|
||||
|
||||
/*
|
||||
* sanity check that:
|
||||
* - decoding base32 succeeded
|
||||
* - the library agrees on how big the output buffer should be, i.e. that the preceding allocation logic was correct
|
||||
*/
|
||||
ok = status == OATH_OK && reportedCapacity == ((size_t) capacity);
|
||||
|
||||
/*
|
||||
* Avoid += because then strlen() is used which:
|
||||
* - Might branch on unitialised memory according to Valgrind
|
||||
* - Does not work well with embedded \0, which might be used and *is* valid
|
||||
*/
|
||||
if (ok) {
|
||||
result.append(output, reportedCapacity);
|
||||
}
|
||||
|
||||
/*
|
||||
* At this point we have an extra copy of the (decoded) secret in memory.
|
||||
* Wipe it and free up the memory.
|
||||
*
|
||||
* Note the +1 for trailing \0: not strictly necessary but good to be aware?
|
||||
*/
|
||||
if (output) {
|
||||
memset(output, '\0', reportedCapacity + 1);
|
||||
free(output);
|
||||
}
|
||||
|
||||
std::optional<QByteArray> r = std::optional<QByteArray>(result);
|
||||
|
||||
return ok ? r : std::nullopt;
|
||||
// based on the amount of padding, determine the exact size of the encoded data
|
||||
int size = paddingIndex - from;
|
||||
switch (until - paddingIndex) {
|
||||
case 0:
|
||||
return determineCapacity(size, 0, 0);
|
||||
case 1:
|
||||
return determineCapacity(size, 7, 4);
|
||||
case 3:
|
||||
return determineCapacity(size, 5, 3);
|
||||
case 4:
|
||||
return determineCapacity(size, 4, 2);
|
||||
case 6:
|
||||
return determineCapacity(size, 2, 1);
|
||||
default:
|
||||
Q_ASSERT_X(false, Q_FUNC_INFO, "invalid input size/amount of padding should have been caught by previous validation");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
namespace base32
|
||||
{
|
||||
std::optional<size_t> validate(const QString &encoded, int from, int until)
|
||||
{
|
||||
int max = until == -1 ? encoded.size() : until;
|
||||
if (!checkInputRange(encoded, from, max)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<int> padding = isBase32(encoded, from, max);
|
||||
return padding ? std::optional<size_t>(requiredCapacity(*padding, from, max)) : std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<size_t> decode(const QString &encoded, char * const out, size_t outlen, int from, int until)
|
||||
{
|
||||
int max = until == -1 ? encoded.size() : until;
|
||||
if (!checkInputRange(encoded, from, max)) {
|
||||
// TODO warn about this
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<int> padding = isBase32(encoded, from, max);
|
||||
if (!padding) {
|
||||
// TODO warn about this
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
size_t needed = requiredCapacity(*padding, from, max);
|
||||
if (outlen < needed) {
|
||||
// TODO warn about this
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
int index;
|
||||
size_t decoded = 0;
|
||||
for(index = from; index < max && decoded < needed; index += 8) {
|
||||
std::optional<size_t> group = decode(encoded, index, max, *padding, decoded, needed, out);
|
||||
Q_ASSERT_X(group, Q_FUNC_INFO, "input should have been fully validated; decoding should succeed");
|
||||
decoded += *group;
|
||||
}
|
||||
|
||||
Q_ASSERT_X(decoded == needed, Q_FUNC_INFO, "number of bytes decoded should match expected output capacity required");
|
||||
Q_ASSERT_X(index == max, Q_FUNC_INFO, "number of characters decoded should match end of the input range exactly");
|
||||
|
||||
return std::optional<size_t>(decoded);
|
||||
}
|
||||
|
||||
std::optional<QByteArray> decode(const QString &encoded)
|
||||
{
|
||||
std::optional<QByteArray> result = std::nullopt;
|
||||
std::optional<size_t> capacity = validate(encoded);
|
||||
|
||||
if (!capacity) {
|
||||
// TODO warn about this
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
QByteArray decoded;
|
||||
decoded.reserve((int) *capacity);
|
||||
decoded.resize((int) *capacity);
|
||||
if (decode(encoded, decoded.data(), *capacity)) {
|
||||
result.emplace(decoded);
|
||||
}
|
||||
// TODO warn if not
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,21 +1,7 @@
|
|||
/*****************************************************************************
|
||||
* Copyright: 2019 Johan Ouwerkerk <jm.ouwerkerk@gmail.com> *
|
||||
* *
|
||||
* This project is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This project is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
****************************************************************************/
|
||||
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
* SPDX-FileCopyrightText: 2019-2020 Johan Ouwerkerk <jm.ouwerkerk@gmail.com>
|
||||
*/
|
||||
#ifndef BASE32_H
|
||||
#define BASE32_H
|
||||
|
||||
|
@ -26,6 +12,9 @@
|
|||
|
||||
namespace base32
|
||||
{
|
||||
std::optional<size_t> validate(const QString &encoded, int from = 0, int until = -1);
|
||||
std::optional<size_t> decode(const QString &encoded, char * const out, size_t outlen, int from = 0, int until = -1);
|
||||
|
||||
std::optional<QByteArray> decode(const QString &input);
|
||||
}
|
||||
#endif
|
||||
|
|
Loading…
Reference in New Issue