Files
deskflow/src/lib/net/SecureUtils.cpp
2026-03-06 08:57:31 -05:00

212 lines
6.4 KiB
C++

/*
* Deskflow -- mouse and keyboard sharing utility
* SPDX-FileCopyrightText: (C) 2025 Deskflow Developers
* SPDX-FileCopyrightText: (C) 2021 Barrier Contributors
* SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception
*/
#include "SecureUtils.h"
#include "base/FinalAction.h"
#include <openssl/evp.h>
#include <openssl/pem.h>
#include <openssl/x509.h>
#include <openssl/x509v3.h>
#include <algorithm>
#include <filesystem>
#include <stdexcept>
namespace deskflow {
namespace {
const EVP_MD *digestForType(QCryptographicHash::Algorithm type)
{
switch (type) {
case QCryptographicHash::Sha1:
return EVP_sha1();
case QCryptographicHash::Sha256:
return EVP_sha256();
default:
break;
}
throw std::runtime_error("Unknown fingerprint type " + std::to_string(static_cast<int>(type)));
}
} // namespace
QString formatSSLFingerprint(const QByteArray &fingerprint, bool enableSeparators)
{
if (enableSeparators)
return fingerprint.toHex(':').toUpper();
else
return fingerprint.toHex().toUpper();
}
Fingerprint sslCertFingerprint(const X509 *cert, QCryptographicHash::Algorithm type)
{
if (!cert) {
throw std::runtime_error("certificate is null");
}
unsigned char digest[EVP_MAX_MD_SIZE];
unsigned int digestLength = 0;
if (int result = X509_digest(cert, digestForType(type), digest, &digestLength); result <= 0) {
throw std::runtime_error("failed to calculate fingerprint, digest result: " + std::to_string(result));
}
QByteArray digestArray(reinterpret_cast<const char *>(digest), digestLength);
return {type, digestArray};
}
void generatePemSelfSignedCert(const QString &path, int keyLength)
{
auto expirationDays = 365;
auto *privateKey = EVP_PKEY_new();
if (!privateKey) {
throw std::runtime_error("could not allocate private key for certificate");
}
auto privateKeyFree = finally([privateKey]() { EVP_PKEY_free(privateKey); });
privateKey = EVP_RSA_gen(keyLength);
auto *cert = X509_new();
if (!cert) {
throw std::runtime_error("could not allocate certificate");
}
auto certFree = finally([cert]() { X509_free(cert); });
ASN1_INTEGER_set(X509_get_serialNumber(cert), 1);
X509_gmtime_adj(X509_get_notBefore(cert), 0);
X509_gmtime_adj(X509_get_notAfter(cert), expirationDays * 24 * 3600);
X509_set_pubkey(cert, privateKey);
auto *name = X509_get_subject_name(cert);
X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC, reinterpret_cast<const unsigned char *>("Deskflow"), -1, -1, 0);
X509_set_issuer_name(cert, name);
X509_sign(cert, privateKey, EVP_sha256());
const std::filesystem::path fsPath = path.toStdString();
#if defined(Q_OS_WIN)
auto fp = _wfopen(fsPath.native().c_str(), L"w");
#else
auto fp = std::fopen(fsPath.native().c_str(), "w");
#endif
if (!fp) {
throw std::runtime_error("could not open certificate output path");
}
auto fileClose = finally([fp]() { std::fclose(fp); });
PEM_write_PrivateKey(fp, privateKey, nullptr, nullptr, 0, nullptr, nullptr);
PEM_write_X509(fp, cert);
}
QString formatSSLFingerprintColumns(const QByteArray &fingerprint)
{
auto kMaxColumns = 24;
QString hex = fingerprint.toHex(':').toUpper();
if (hex.isEmpty()) {
return hex;
}
QString formattedString;
while (!hex.isEmpty()) {
const auto take = std::min<size_t>(kMaxColumns, hex.size());
formattedString.append(hex.first(take));
hex.remove(0, take);
if (formattedString.endsWith(QLatin1Char(':')))
formattedString.removeLast();
formattedString.append(QLatin1Char('\n'));
}
formattedString.removeLast();
return formattedString;
}
/*
Draw an ASCII-Art representing the fingerprint so human brain can
profit from its built-in pattern recognition ability.
This technique is called "random art" and can be found in some
scientific publications like this original paper:
"Hash Visualization: a New Technique to improve Real-World Security",
Perrig A. and Song D., 1999, International Workshop on Cryptographic
Techniques and E-Commerce (CrypTEC '99)
sparrow.ece.cmu.edu/~adrian/projects/validation/validation.pdf
The subject came up in a talk by Dan Kaminsky, too.
If you see the picture is different, the key is different.
If the picture looks the same, you still know nothing.
The algorithm used here is a worm crawling over a discrete plane,
leaving a trace (augmenting the field) everywhere it goes.
Movement is taken from rawDigest 2bit-wise. Bumping into walls
makes the respective movement vector be ignored for this turn.
Graphs are not unambiguous, because circles in graphs can be
walked in either direction.
*/
/*
Field sizes for the random art. Have to be odd, so the starting point
can be in the exact middle of the picture, and `baseSize` should be >=8 .
Else pictures would be too dense, and drawing the frame would
fail, too, because the key type would not fit in anymore.
*/
QString generateFingerprintArt(const QByteArray &rawDigest)
{
const auto baseSize = 8;
const auto rows = (baseSize + 1);
const auto columns = (baseSize * 2 + 1);
const auto characterPool = QStringLiteral(" .o+=*BOX@%&#/^SE");
const std::size_t len = characterPool.length() - 1;
std::uint8_t field[columns][rows];
memset(field, 0, columns * rows * sizeof(char));
int x = columns / 2;
int y = rows / 2;
/* process raw key */
for (int byte : rawDigest) {
/* each byte conveys four 2-bit move commands */
for (uint32_t b = 0; b < 4; b++) {
/* evaluate 2 bit, rest is shifted later */
x += (byte & 0x1) ? 1 : -1;
y += (byte & 0x2) ? 1 : -1;
/* assure we are still in bounds */
x = std::clamp(x, 0, columns - 1);
y = std::clamp(y, 0, rows - 1);
/* augment the field */
if (field[x][y] < len - 2)
field[x][y]++;
byte = byte >> 2;
}
}
/* mark starting point and end point*/
field[columns / 2][rows / 2] = len - 1;
field[x][y] = len;
QString result;
result.reserve((columns + 3) * (rows + 2));
result.append(QStringLiteral("╔═════════════════╗\n"));
/* output content */
for (y = 0; y < rows; y++) {
result.append(QStringLiteral(""));
for (x = 0; x < columns; x++)
result.append(characterPool.at(std::min<int>(field[x][y], len)));
result.append(QStringLiteral("\n"));
}
result.append(QStringLiteral("╚═════════════════╝"));
return result;
}
} // namespace deskflow