From 2f86c1e9a667714e95bb1c35294e5fcb988803ef Mon Sep 17 00:00:00 2001 From: acetone <63557806+acetoneRu@users.noreply.github.com> Date: Sat, 23 Apr 2022 15:59:34 -0400 Subject: [PATCH] main sources --- zerostoragecaptcha.cpp | 272 +++++++++++++++++++++++++++++++++++ zerostoragecaptcha.h | 120 ++++++++++++++++ zerostoragecaptchacrypto.cpp | 151 +++++++++++++++++++ zerostoragecaptchacrypto.h | 58 ++++++++ 4 files changed, 601 insertions(+) create mode 100644 zerostoragecaptcha.cpp create mode 100644 zerostoragecaptcha.h create mode 100644 zerostoragecaptchacrypto.cpp create mode 100644 zerostoragecaptchacrypto.h diff --git a/zerostoragecaptcha.cpp b/zerostoragecaptcha.cpp new file mode 100644 index 0000000..72fd69f --- /dev/null +++ b/zerostoragecaptcha.cpp @@ -0,0 +1,272 @@ +// 2022 (c) GPLv3, acetone at i2pmail.org +// Zero Storage Captcha + +/* +* Copyright (c) 2014 Omkar Kanase +* QtCaptcha: https://github.com/omkar-developer/QtCaptcha +* +* This software is provided 'as-is', without any express or implied +* warranty. In no event will the authors be held liable for any damages +* arising from the use of this software. +* Permission is granted to anyone to use this software for any purpose, +* including commercial applications, and to alter it and redistribute it +* freely, subject to the following restrictions: +* 1. The origin of this software must not be misrepresented; you must not +* claim that you wrote the original software. If you use this software +* in a product, an acknowledgment in the product documentation would be +* appreciated but is not required. +* 2. Altered source versions must be plainly marked as such, and must not be +* misrepresented as being the original software. +* 3. This notice may not be removed or altered from any source distribution. +*/ + +#include "zerostoragecaptcha.h" + +#include +#include +#include +#include +#include + +ZeroStorageCaptcha::ZeroStorageCaptcha() +{ + if (not ZeroStorageCaptchaCrypto::TimeToken::inited()) ZeroStorageCaptchaCrypto::TimeToken::init(); + + m_hmod1 = 0.0; + m_hmod2 = 0.0; + + m_vmod1 = 0.0; + m_vmod2 = 0.0; + + m_font.setStyleStrategy(QFont::ForceOutline); + m_font.setPointSize(50); + m_font.setBold(true); + m_font.setLetterSpacing(QFont::PercentageSpacing, QFont::SemiCondensed); + + m_captchaImage = QImage(200, 100, QImage::Format_RGB32); + + if (QTime::currentTime().msec() % 2 == 0) + { + m_backColor = Qt::GlobalColor::white; + m_fontColor = Qt::GlobalColor::black; + } + else + { + m_backColor = Qt::GlobalColor::black; + m_fontColor = Qt::GlobalColor::white; + } + + m_padding = 5; + + setDifficulty(3); + m_captchaText = "NOTSET"; + + qsrand(static_cast(QTime::currentTime().msec())); // randomize +} + +bool ZeroStorageCaptcha::validate(const QString &answer, const QString &token) +{ + return ZeroStorageCaptchaCrypto::KeyHolder::validateCaptchaAnswer(answer, token); +} + +ZeroStorageCaptchaContainer ZeroStorageCaptcha::getCaptcha(int length, int difficulty) +{ + ZeroStorageCaptcha c; + c.setDifficulty(difficulty); + c.generateText(length); + return ZeroStorageCaptchaContainer (c.captchaPngByteArray(), c.captchaToken(), c.captchaText()); +} + +QString ZeroStorageCaptcha::captchaToken() const +{ + return ZeroStorageCaptchaCrypto::KeyHolder::captchaSecretLine(m_captchaText); +} + +QByteArray ZeroStorageCaptcha::captchaPngByteArray() const +{ + QByteArray data; + QBuffer buff(&data); + m_captchaImage.save(&buff, "PNG"); + return data; +} + +void ZeroStorageCaptcha::updateCaptcha() +{ + QPainterPath path; + QFontMetrics fm(m_font); + + path.addText(m_vmod2 + m_padding, m_hmod2 - m_padding + fm.height(), font(), captchaText()); + + qreal sinrandomness = (static_cast(qrand()) / RAND_MAX) * 5.0; + + for (int i = 0; i < path.elementCount(); ++i) + { + const QPainterPath::Element& el = path.elementAt(i); + qreal y = el.y + sin(el.x / m_hmod1 + sinrandomness) * m_hmod2; + qreal x = el.x + sin(el.y / m_vmod1 + sinrandomness) * m_vmod2; + path.setElementPositionAt(i, x, y); + } + + m_captchaImage = QImage(static_cast(fm.horizontalAdvance(m_captchaText) + m_vmod2 * 2 + m_padding * 2), + static_cast(fm.height() + m_hmod2 * 2 + m_padding * 2), QImage::Format_RGB32); + + m_captchaImage.fill(backColor()); + + QPainter painter; + painter.begin(&m_captchaImage); + painter.setPen(Qt::NoPen); + painter.setBrush(fontColor()); + painter.setRenderHint(QPainter::Antialiasing); + painter.drawPath(path); + + if (m_drawLines) + { + painter.setPen(QPen(Qt::black, m_lineWidth)); + for (int i = 0; i < m_lineCount; i++) + { + int x1 = (static_cast(qrand()) / RAND_MAX) * m_captchaImage.width(); + int y1 = (static_cast(qrand()) / RAND_MAX) * m_captchaImage.height(); + int x2 = (static_cast(qrand()) / RAND_MAX) * m_captchaImage.width(); + int y2 = (static_cast(qrand()) / RAND_MAX) * m_captchaImage.height(); + painter.drawLine(x1, y1, x2, y2); + } + painter.setPen(Qt::NoPen); + } + + if (m_drawEllipses) + { + for (int i = 0; i < m_ellipseCount; i++) + { + int x1 = static_cast(m_ellipseMaxRadius / 2.0 + (static_cast(qrand()) / RAND_MAX) * (m_captchaImage.width() - m_ellipseMaxRadius)); + int y1 = static_cast(m_ellipseMaxRadius / 2.0 + (static_cast(qrand()) / RAND_MAX) * (m_captchaImage.height() - m_ellipseMaxRadius)); + int rad1 = static_cast(m_ellipseMinRadius + (static_cast(qrand()) / RAND_MAX) * (m_ellipseMaxRadius - m_ellipseMinRadius)); + int rad2 = static_cast(m_ellipseMinRadius + (static_cast(qrand()) / RAND_MAX) * (m_ellipseMaxRadius - m_ellipseMinRadius)); + if (backColor() == Qt::GlobalColor::black) + { + painter.setBrush(fontColor()); + painter.setCompositionMode(QPainter::CompositionMode_Difference); + } else { + painter.setBrush(backColor()); + painter.setCompositionMode(QPainter::CompositionMode_Exclusion); + } + painter.drawEllipse(QPoint(x1, y1), rad1, rad2); + } + } + + if (m_drawNoise) + { + for (int i = 0; i < m_noiseCount; i++) + { + int x1 = static_cast(static_cast(qrand()) / RAND_MAX * m_captchaImage.width()); + int y1 = static_cast(static_cast(qrand()) / RAND_MAX * m_captchaImage.height()); + + QColor col = backColor() == Qt::GlobalColor::black ? Qt::GlobalColor::white : Qt::GlobalColor::black; + + painter.setPen(QPen(col, m_noisePointSize)); + painter.setCompositionMode(QPainter::CompositionMode_SourceOver); + painter.drawPoint(x1, y1); + } + } + + painter.end(); +} + +void ZeroStorageCaptcha::setSinDeform(qreal hAmplitude, qreal hFrequency, qreal vAmplitude, qreal vFrequency) +{ + m_hmod1 = hFrequency; + m_hmod2 = hAmplitude; + m_vmod1 = vFrequency; + m_vmod2 = vAmplitude; +} + +void ZeroStorageCaptcha::setDifficulty(int val) +{ + if (val < 0 or val > 5) + { + qInfo() << QString(__PRETTY_FUNCTION__) << "Min difficulty is 0, maximal is 5"; + } + + if (val < 1) + { + m_drawLines = false; + m_drawEllipses = false; + m_drawNoise = false; + setSinDeform(10, 10, 5, 20); + } + else if (val == 1) + { + m_drawLines = true; + m_lineWidth = 3; + m_lineCount = 5; + m_drawEllipses = false; + m_drawNoise = false; + setSinDeform(10, 15, 5, 20); + } + else if (val == 2) + { + m_drawLines = true; + m_lineWidth = 2; + m_lineCount = 5; + m_drawEllipses = true; + m_ellipseCount = 1; + m_ellipseMinRadius = 20; + m_ellipseMaxRadius = 40; + m_drawNoise = false; + setSinDeform(10, 15, 5, 15); + } + else if (val == 3) + { + m_drawLines = true; + m_lineWidth = 2; + m_lineCount = 3; + m_drawEllipses = true; + m_ellipseCount = 1; + m_ellipseMinRadius = 20; + m_ellipseMaxRadius = 50; + m_drawNoise = true; + m_noiseCount = 100; + m_noisePointSize = 3; + setSinDeform(8, 13, 5, 15); + } + else if (val == 4) + { + m_drawLines = true; + m_lineWidth = 3; + m_lineCount = 5; + m_drawEllipses = true; + m_ellipseCount = 2; + m_ellipseMinRadius = 20; + m_ellipseMaxRadius = 40; + m_drawNoise = true; + m_noiseCount = 100; + m_noisePointSize = 3; + setSinDeform(8, 13, 5, 15); + } + else + { + m_drawLines = true; + m_lineWidth = 4; + m_lineCount = 7; + m_drawEllipses = true; + m_ellipseCount = 1; + m_ellipseMinRadius = 20; + m_ellipseMaxRadius = 40; + m_drawNoise = true; + m_noiseCount = 200; + m_noisePointSize = 3; + setSinDeform(8, 10, 5, 10); + } +} + +void ZeroStorageCaptcha::generateText(int length) +{ + if (length <= 0) + { + qInfo() << QString(__PRETTY_FUNCTION__) << "Invalid number of characters. Set to 5."; + length = 5; + } + + m_captchaText = ZeroStorageCaptchaCrypto::random(length); + + updateCaptcha(); +} diff --git a/zerostoragecaptcha.h b/zerostoragecaptcha.h new file mode 100644 index 0000000..e75ed07 --- /dev/null +++ b/zerostoragecaptcha.h @@ -0,0 +1,120 @@ +// 2022 (c) GPLv3, acetone at i2pmail.org +// Zero Storage Captcha + +/* +* Copyright (c) 2014 Omkar Kanase +* QtCaptcha: https://github.com/omkar-developer/QtCaptcha +* +* This software is provided 'as-is', without any express or implied +* warranty. In no event will the authors be held liable for any damages +* arising from the use of this software. +* Permission is granted to anyone to use this software for any purpose, +* including commercial applications, and to alter it and redistribute it +* freely, subject to the following restrictions: +* 1. The origin of this software must not be misrepresented; you must not +* claim that you wrote the original software. If you use this software +* in a product, an acknowledgment in the product documentation would be +* appreciated but is not required. +* 2. Altered source versions must be plainly marked as such, and must not be +* misrepresented as being the original software. +* 3. This notice may not be removed or altered from any source distribution. +*/ + +#ifndef ZEROSTORAGECAPTCHA_H +#define ZEROSTORAGECAPTCHA_H + +#include "zerostoragecaptchacrypto.h" + +#include +#include + +class ZeroStorageCaptchaContainer +{ +public: + ZeroStorageCaptchaContainer(const QByteArray& pic, const QString& token, const QString& answer) : + m_picture(pic), + m_token(token), + m_answer(answer) + {} + + const QByteArray& picture() const { return m_picture; } + const QString& token() const { return m_token; } + const QString& answer() const { return m_answer; } + +private: + QByteArray m_picture; + QString m_token; + QString m_answer; +}; + +class ZeroStorageCaptcha +{ +public: + ZeroStorageCaptcha(); + static bool validate(const QString& answer, const QString& token); + static ZeroStorageCaptchaContainer getCaptcha(int length = 5, int difficulty = 3); + + QString captchaText() const { return m_captchaText; } + QString captchaToken() const; + QByteArray captchaPngByteArray() const; + + QImage captchaImage() const { return m_captchaImage; } + QFont font() const { return m_font; } + QColor fontColor() const { return m_fontColor; } + QColor backColor() const { return m_backColor; } + bool drawLines() const { return m_drawLines; } + bool drawEllipses() const { return m_drawLines; } + bool drawNoise() const { return m_drawLines; } + int noiseCount() const { return m_noiseCount; } + int lineCount() const { return m_lineCount; } + int ellipseCount() const { return m_ellipseCount; } + int lineWidth() const { return m_lineWidth; } + int ellipseMinRadius() const { return m_ellipseMinRadius; } + int ellipseMaxRadius() const { return m_ellipseMaxRadius; } + int noisePointSize() const { return m_noisePointSize; } + + void setFont(const QFont& arg) { m_font = arg; } + void setCaptchaText(QString arg) { m_captchaText = arg; } + void setFontColor(QColor arg) { m_fontColor = arg; } + void setBackColor(QColor arg) { m_backColor = arg; } + void setDrawLines(bool arg) { m_drawLines = arg; } + void setDrawEllipses(bool arg) { m_drawEllipses = arg; } + void setDrawNoise(bool arg) { m_drawNoise = arg; } + void setNoiseCount(int arg) { m_noiseCount = arg; } + void setLineCount(int arg) { m_lineCount = arg; } + void setEllipseCount(int arg) { m_ellipseCount = arg; } + void setLineWidth(int arg) { m_lineWidth = arg; } + void setEllipseMinRadius(int arg) { m_ellipseMinRadius = arg; } + void setEllipseMaxRadius(int arg) { m_ellipseMaxRadius = arg; } + void setNoisePointSize(int arg) { m_noisePointSize = arg; } + void setSinDeform(qreal hAmplitude, qreal hFrequency, qreal vAmplitude, qreal vFrequency); + void setDifficulty(int val); + void generateText(int length = 5); + void updateCaptcha(); + +private: + qreal m_hmod1; + qreal m_hmod2; + + qreal m_vmod1; + qreal m_vmod2; + + QFont m_font; + QImage m_captchaImage; + QString m_captchaText; + QColor m_fontColor; + QColor m_backColor; + qreal m_padding; + bool m_drawLines; + bool m_drawEllipses; + bool m_drawNoise; + int m_noiseCount; + int m_lineCount; + int m_ellipseCount; + int m_lineWidth; + int m_ellipseMinRadius; + int m_ellipseMaxRadius; + int m_noisePointSize; +}; + +#endif // ZEROSTORAGECAPTCHA_H diff --git a/zerostoragecaptchacrypto.cpp b/zerostoragecaptchacrypto.cpp new file mode 100644 index 0000000..5a3a502 --- /dev/null +++ b/zerostoragecaptchacrypto.cpp @@ -0,0 +1,151 @@ +// 2022 (c) GPLv3, acetone at i2pmail.org +// Zero Storage Captcha + +#include "zerostoragecaptchacrypto.h" +#include "timetoken.h" + +#include +#include +#include +#include + +constexpr int TIME_TOKEN_SIZE = 10; + +namespace ZeroStorageCaptchaCrypto { + +QTimer* TimeToken::m_updater = nullptr; +QString TimeToken::m_current; +QString TimeToken::m_prev; +bool KeyHolder::m_caseSensitive = false; +uint8_t KeyHolder::m_key[KEYSIZE] {0}; + +void TimeToken::init() +{ + if (m_updater) return; + + m_updater = new QTimer; + m_current = ZeroStorageCaptchaCrypto::random(TIME_TOKEN_SIZE); + m_updater->setInterval(2000); // 2 minutes + QObject::connect(m_updater, &QTimer::timeout, [&]() { + m_prev = m_current; + m_current = ZeroStorageCaptchaCrypto::random(TIME_TOKEN_SIZE); + }); + m_updater->start(); +} + +QString KeyHolder::captchaSecretLine(const QString &captchaAnswer, bool prevTimeToken) +{ + if (m_key[0] == 0) + { + auto noize = ZeroStorageCaptchaCrypto::random(KEYSIZE); + for (int i = 0; i < KEYSIZE; ++i) + { + m_key[i] = noize[i]; + } + } + + QString hashedAnswer = ZeroStorageCaptchaCrypto::hash((m_caseSensitive ? captchaAnswer : captchaAnswer.toUpper()) + + (prevTimeToken ? TimeToken::prevToken() : TimeToken::currentToken()) ); + + uint8_t signature[SIGSIZE]; + sign(reinterpret_cast(hashedAnswer.toStdString().c_str()), hashedAnswer.size(), signature, m_key); + + QByteArray rawResultArray; + for(int i = 0; i < SIGSIZE; ++i) + { + rawResultArray += signature[i]; + } + + return compact(rawResultArray.toBase64(QByteArray::Base64Option::Base64UrlEncoding)); +} + +bool KeyHolder::validateCaptchaAnswer(const QString &answer, const QString &secretLine) +{ + if (captchaSecretLine(answer) == secretLine) return true; + return captchaSecretLine(answer, true) == secretLine; +} + +QString KeyHolder::compact(const QString &str) +{ + QString result; + int counter = 0; + for (const auto& c: str) + { + if (++counter % 3 == 0) + { + result += c; + } + } + result.remove('='); + result.remove('_'); + result.remove('-'); + return result; +} + +void KeyHolder::sign(const uint8_t *buf, int len, uint8_t *signature, const uint8_t *privateKey) +{ + auto MDCtx = EVP_MD_CTX_create (); + auto PKey = EVP_PKEY_new_raw_private_key (EVP_PKEY_ED25519, NULL, privateKey, KEYSIZE); + EVP_DigestSignInit (MDCtx, NULL, NULL, NULL, PKey); + + size_t l = SIGSIZE; + + EVP_DigestSign (MDCtx, signature, &l, buf, len); + + EVP_PKEY_free (PKey); + EVP_MD_CTX_destroy (MDCtx); +} + +QString hash(const QString &str) +{ + // Partially bit-inverted SHA256 + QVector in; + for(auto c: str) + { + in.push_back(c.unicode()); + } + + QVector out(SHA256_DIGEST_LENGTH); + SHA256(in.data(), in.size(), out.data()); + + QByteArray rawResult; + for (auto b: out) + { + rawResult.push_back(b); + } + + short count = 0; + for (auto it = rawResult.begin(); it != rawResult.end(); ++it) + { + if (++count % 2 == 0) + { + *it = ~*it; + } + } + + return rawResult.toBase64(QByteArray::Base64Option::Base64UrlEncoding); +} + +QByteArray random(int length) +{ + constexpr char randomtable[60] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', + 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', + 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', + 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', + 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X'}; + + QByteArray random_value; + + std::random_device rd; + std::uniform_int_distribution dist(0, 59); + + while(random_value.size() < length) + { + random_value += randomtable[dist(rd)]; + } + + return random_value; +} + +} // namespace diff --git a/zerostoragecaptchacrypto.h b/zerostoragecaptchacrypto.h new file mode 100644 index 0000000..63536d6 --- /dev/null +++ b/zerostoragecaptchacrypto.h @@ -0,0 +1,58 @@ +// 2022 (c) GPLv3, acetone at i2pmail.org +// Zero Storage Captcha + +#ifndef ZEROSTORAGECAPTCHACRYPTO_H +#define ZEROSTORAGECAPTCHACRYPTO_H + +#include +#include +#include + +constexpr int KEYSIZE = 32; +constexpr int SIGSIZE = 64; + +namespace ZeroStorageCaptchaCrypto { + +QString hash(const QString& str); +QByteArray random(int length); + +///////// + +class TimeToken +{ +public: + TimeToken() = delete; + + static void init(); + static bool inited() { return m_updater; } + static const QString currentToken() { return m_current; } + static const QString prevToken() { return m_prev; } + +private: + static QTimer* m_updater; + static QString m_current; + static QString m_prev; +}; + +///////// + +class KeyHolder +{ +public: + KeyHolder() = delete; + + static QString captchaSecretLine(const QString& captchaAnswer, bool prevTimeToken = false); + static bool validateCaptchaAnswer(const QString& answer, const QString& secretLine); + static void setCaseSensitive(bool enabled = false) { m_caseSensitive = enabled; } + +private: + static QString compact(const QString& str); + static void sign(const uint8_t * buf, int len, uint8_t * signature, const uint8_t * privateKey); + + static bool m_caseSensitive; + static uint8_t m_key[KEYSIZE]; +}; + +} // namespace + +#endif // ZEROSTORAGECAPTCHACRYPTO_H