212 lines
6.4 KiB
C++
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
|