#include "systemController.h" #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { constexpr int SALT_LEN = 16; constexpr int IV_LEN = 12; constexpr int KEY_LEN = 32; constexpr int TAG_LEN = 16; constexpr int PBKDF2_ITER = 100000; const QByteArray magicString { "EncData" }; } #ifdef Q_OS_ANDROID #include "platforms/android/android_controller.h" #endif #if defined(Q_OS_IOS) || defined(MACOS_NE) #include "platforms/ios/ios_controller.h" #include #endif SystemController::SystemController(QObject *parent) : QObject(parent) { } bool SystemController::saveFile(const QString &fileName, const QString &data) { #if defined Q_OS_ANDROID AndroidController::instance()->saveFile(fileName, data); return true; #endif return saveFile(fileName, data.toUtf8()); } bool SystemController::saveFile(const QString &fileName, const QByteArray &data) { #if defined Q_OS_ANDROID AndroidController::instance()->saveFile(fileName, QString::fromUtf8(data)); return true; #endif #ifdef Q_OS_IOS QUrl fileUrl = QDir::tempPath() + "/" + fileName; QFile file(fileUrl.toString()); #else QFile file(fileName); #endif if (!file.open(QIODevice::WriteOnly)) { qWarning() << "SystemController::saveFile: cannot open" << fileName; return false; } if (file.write(data) != data.size()) { qWarning() << "SystemController::saveFile: write failed" << fileName; file.close(); return false; } file.close(); #ifdef Q_OS_IOS QStringList filesToSend; filesToSend.append(fileUrl.toString()); return IosController::Instance()->shareText(filesToSend); #else QFileInfo fi(fileName); #ifdef Q_OS_MAC const auto url = "file://" + fi.absoluteDir().absolutePath(); #else const auto url = fi.absoluteDir().absolutePath(); #endif #ifndef MACOS_NE QDesktopServices::openUrl(url); #endif return true; #endif } bool SystemController::readFile(const QString &fileName, QByteArray &data) { #ifdef Q_OS_ANDROID int fd = AndroidController::instance()->getFd(fileName); if (fd == -1) return false; QFile file; if(!file.open(fd, QIODevice::ReadOnly)) return false; data = file.readAll(); AndroidController::instance()->closeFd(); #else QFile file(fileName); if (!file.open(QIODevice::ReadOnly)) return false; data = file.readAll(); #endif return true; } bool SystemController::readFile(const QString &fileName, QString &data) { QByteArray byteArray; if(!readFile(fileName, byteArray)) return false; data = byteArray; return true; } static QString opensslErrString() { unsigned long e = ERR_get_error(); if (!e) return QStringLiteral("Unknown OpenSSL error"); char buf[256]; ERR_error_string_n(e, buf, sizeof(buf)); return QString::fromUtf8(buf); } static bool deriveKey(const QByteArray &password, const QByteArray &salt, QByteArray &outKey) { outKey.resize(KEY_LEN); const unsigned char *pw = reinterpret_cast(password.constData()); const unsigned char *s = reinterpret_cast(salt.constData()); int ok = PKCS5_PBKDF2_HMAC(reinterpret_cast(pw), password.size(), s, salt.size(), PBKDF2_ITER, EVP_sha256(), KEY_LEN, reinterpret_cast(outKey.data())); if (!ok) { qDebug() << opensslErrString(); } return ok == 1; } static bool aesCrypt(const QByteArray &in, const QByteArray &key, const QByteArray &iv, QByteArray &out, QByteArray &tag, bool encrypt) { std::unique_ptr ctx { EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_free }; if (!ctx) { qDebug() << "EVP_CIPHER_CTX_new failed"; return false; } const EVP_CIPHER *cipher = EVP_aes_256_gcm(); if (1 != EVP_CipherInit_ex(ctx.get(), cipher, nullptr, nullptr, nullptr, encrypt ? 1 : 0)) { qDebug() << opensslErrString(); return false; } if (1 != EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_GCM_SET_IVLEN, iv.size(), nullptr)) { qDebug() << opensslErrString(); return false; } if (1 != EVP_CipherInit_ex(ctx.get(), nullptr, nullptr, reinterpret_cast(key.constData()), reinterpret_cast(iv.constData()), -1)) { qDebug() << opensslErrString(); return false; } out.clear(); out.resize(in.size()); int outlen = 0; if (1 != EVP_CipherUpdate(ctx.get(), reinterpret_cast(out.data()), &outlen, reinterpret_cast(in.constData()), in.size())) { qDebug() << opensslErrString(); return false; } int tmplen = 0; if (encrypt) { if (1 != EVP_CipherFinal_ex(ctx.get(), reinterpret_cast(out.data()) + outlen, &tmplen)) { qDebug() << opensslErrString(); return false; } out.resize(outlen + tmplen); tag.resize(TAG_LEN); if (1 != EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_GCM_GET_TAG, TAG_LEN, tag.data())) { qDebug() << opensslErrString(); return false; } } else { if (1 != EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_GCM_SET_TAG, tag.size(), const_cast(tag.constData()))) { qDebug() << opensslErrString(); return false; } if (1 != EVP_CipherFinal_ex(ctx.get(), reinterpret_cast(out.data()) + outlen, &tmplen)) { qDebug() << "Authentication failed: " << opensslErrString(); return false; } out.resize(outlen + tmplen); } return true; } bool SystemController::encryptFile(const QString &filePath, const QString &password, const QString &hint) { QFile f(filePath); if (!f.open(QIODevice::ReadOnly)) { qDebug() << "Cannot open file for read: " << f.errorString(); return false; } QByteArray content = f.readAll(); f.close(); if (content.startsWith(magicString)) { qDebug() << "File already encrypted"; return false; } QByteArray salt(SALT_LEN, 0); QByteArray iv(IV_LEN, 0); QByteArray key; QByteArray cipher; QByteArray tag; if (1 != RAND_bytes(reinterpret_cast(salt.data()), SALT_LEN) || 1 != RAND_bytes(reinterpret_cast(iv.data()), IV_LEN)) { qDebug() << opensslErrString(); return false; } if (!deriveKey(password.toUtf8(), salt, key)) return false; if (!aesCrypt(content, key, iv, cipher, tag, true)) return false; QByteArray out; QByteArray hintBytes = hint.toUtf8(); quint32 hintLen = static_cast(hintBytes.size()); out += magicString; out.append(reinterpret_cast(&hintLen), sizeof(hintLen)); out += hintBytes; out += salt; out += iv; out += tag; out += cipher; if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate)) { qDebug() << "Cannot open file for write: " << f.errorString(); return false; } if (f.write(out) != out.size()) { qDebug() << "Write failed"; f.close(); return false; } f.close(); return true; } QByteArray SystemController::getDecryptedData(const QString &filePath, const QString &password) { QFile f(filePath); if (!f.open(QIODevice::ReadOnly)) { qDebug() << "Cannot open file: " << f.errorString(); return {}; } QByteArray content = f.readAll(); f.close(); if (!content.startsWith(magicString)) { qDebug() << "Invalid file format (magic missing)"; return {}; } int pos = magicString.size(); if (content.size() < pos + static_cast(sizeof(quint32))) { qDebug() << "Corrupted file (no hint length)"; return {}; } quint32 hintLen = 0; memcpy(&hintLen, content.constData() + pos, sizeof(quint32)); pos += sizeof(quint32); if (content.size() < pos + static_cast(hintLen) + SALT_LEN + IV_LEN + TAG_LEN) { qDebug() << "Corrupted file (invalid sizes)"; return {}; } pos += hintLen; QByteArray salt = content.mid(pos, SALT_LEN); pos += SALT_LEN; QByteArray iv = content.mid(pos, IV_LEN); pos += IV_LEN; QByteArray tag = content.mid(pos, TAG_LEN); pos += TAG_LEN; QByteArray cipher = content.mid(pos); QByteArray key; if (!deriveKey(password.toUtf8(), salt, key)) { qDebug() << "Key derivation failed"; return {}; } QByteArray plain; if (!aesCrypt(cipher, key, iv, plain, tag, false)) { qDebug() << "Decryption failed (wrong password or corrupted data)"; return {}; } return plain; } bool SystemController::isFileEncrypted(const QString &filePath) { QFile f(filePath); if (!f.open(QIODevice::ReadOnly)) { qDebug() << "Cannot open file for read: %1", f.errorString(); return false; } QByteArray data = f.readAll(); f.close(); if (!data.startsWith(magicString)) { qDebug() << "File is not recognized as encrypted (magic missing)"; return false; } return true; } bool SystemController::isPasswordValid(const QString &filePath, const QString &password) { QFile f(filePath); if (!f.open(QIODevice::ReadOnly)) { qDebug() << f.errorString(); return false; } QByteArray content = f.readAll(); f.close(); if (!content.startsWith(magicString)) return false; int pos = magicString.size(); quint32 hintLen = 0; memcpy(&hintLen, content.constData() + pos, sizeof(quint32)); pos += sizeof(quint32); pos += hintLen; QByteArray salt = content.mid(pos, SALT_LEN); pos += SALT_LEN; QByteArray iv = content.mid(pos, IV_LEN); pos += IV_LEN; QByteArray tag = content.mid(pos, TAG_LEN); pos += TAG_LEN; QByteArray cipher = content.mid(pos); QByteArray key; if (!deriveKey(password.toUtf8(), salt, key)) return false; QByteArray plain; bool ok = aesCrypt(cipher, key, iv, plain, tag, false); if (!ok) qDebug() << "Wrong password"; return ok; } QString SystemController::readHint(const QString &filePath) { if (filePath.isEmpty()) return ""; QByteArray data; readFile(filePath, data); if (!data.startsWith(magicString)) { qDebug() << "Not an encrypted file"; return {}; } int pos = magicString.size(); if (data.size() < pos + static_cast(sizeof(quint32))) { qDebug() << "Corrupted file (no hint length)"; return {}; } quint32 hintLen = 0; memcpy(&hintLen, data.constData() + pos, sizeof(quint32)); pos += sizeof(quint32); if (data.size() < pos + static_cast(hintLen)) { qDebug() << "Corrupted file (hint truncated)"; return {}; } return QString::fromUtf8(data.constData() + pos, hintLen); } QString SystemController::getFileName(const QString &acceptLabel, const QString &nameFilter, const QString &selectedFile, const bool isSaveMode, const QString &defaultSuffix) { QString fileName; #ifdef Q_OS_ANDROID Q_ASSERT(!isSaveMode); return AndroidController::instance()->openFile(nameFilter); #endif #ifdef Q_OS_IOS fileName = IosController::Instance()->openFile(); if (fileName.isEmpty()) { return fileName; } CFURLRef url = CFURLCreateWithFileSystemPath( kCFAllocatorDefault, CFStringCreateWithCharacters(0, reinterpret_cast(fileName.unicode()), fileName.length()), kCFURLPOSIXPathStyle, 0); if (!CFURLStartAccessingSecurityScopedResource(url)) { qDebug() << "Could not access path " << QUrl::fromLocalFile(fileName).toString(); } return fileName; #endif QObject *mainFileDialog = m_qmlRoot->findChild("mainFileDialog").parent(); if (!mainFileDialog) { return ""; } mainFileDialog->setProperty("acceptLabel", QVariant::fromValue(acceptLabel)); mainFileDialog->setProperty("nameFilters", QVariant::fromValue(QStringList(nameFilter))); mainFileDialog->setProperty("defaultSuffix", QVariant::fromValue(defaultSuffix)); mainFileDialog->setProperty("isSaveMode", QVariant::fromValue(isSaveMode)); if (!selectedFile.isEmpty()) { mainFileDialog->setProperty("selectedFile", QVariant::fromValue(QUrl(selectedFile))); } QMetaObject::invokeMethod(mainFileDialog, "open"); bool isFileDialogAccepted = false; QEventLoop wait; QObject::connect(this, &SystemController::fileDialogClosed, [&wait, &isFileDialogAccepted](const bool isAccepted) { isFileDialogAccepted = isAccepted; wait.quit(); }); wait.exec(); QObject::disconnect(this, &SystemController::fileDialogClosed, nullptr, nullptr); if (!isFileDialogAccepted) { return ""; } fileName = mainFileDialog->property("selectedFile").toString(); return QUrl(fileName).toLocalFile(); } void SystemController::setQmlRoot(QObject *qmlRoot) { m_qmlRoot = qmlRoot; } bool SystemController::isAuthenticated() { #ifdef Q_OS_ANDROID return AndroidController::instance()->requestAuthentication(); #else return true; #endif } void SystemController::sendTouch(float x, float y) { #ifdef Q_OS_ANDROID AndroidController::instance()->sendTouch(x, y); #endif }