feat: encrypt saved server profile tokens

This commit is contained in:
dela
2026-06-22 13:51:10 +08:00
parent 4b650d4152
commit 7979a108c9
5 changed files with 306 additions and 12 deletions
+4 -1
View File
@@ -1,6 +1,7 @@
#include "configbundle.h"
#include "../../config/configstore.h"
#include "../../utils/serverprofilecrypto.h"
#include <QCoreApplication>
#include <QDateTime>
@@ -304,7 +305,9 @@ ConfigBundle ConfigBundle::collectFromLocal()
f.close();
const QJsonDocument doc = QJsonDocument::fromJson(bytes);
if (doc.isArray()) {
b.servers = doc.array();
bool migrated = false;
b.servers =
ServerProfileCrypto::decryptServerArray(doc.array(), &migrated);
} else {
qWarning() << "[ConfigBundle] servers.json is not array, skipped";
}
@@ -1,11 +1,25 @@
#include "servermanager.h"
#include "../../api/embywebsocket.h"
#include "../../utils/serverprofilecrypto.h"
#include <QDebug>
#include <QJsonArray>
#include <QJsonDocument>
#include <QFile>
#include <QFileInfo>
#include <QStandardPaths>
#include <QDir>
namespace
{
QString serversJsonPath()
{
const QString dir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
return QDir(dir).filePath(QStringLiteral("servers.json"));
}
} // namespace
ServerManager::ServerManager(NetworkManager* nm, QObject* parent)
: QObject(parent), m_network(nm) {
loadSettings();
@@ -181,20 +195,55 @@ void ServerManager::saveSettings() {
array.append(obj);
}
QString path = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
QDir().mkpath(path);
QFile file(path + "/servers.json");
if (file.open(QIODevice::WriteOnly)) {
file.write(QJsonDocument(array).toJson());
const QString filePath = serversJsonPath();
const QString dirPath = QFileInfo(filePath).absolutePath();
if (!QDir().mkpath(dirPath)) {
qWarning() << "[ServerManager] failed to create server config dir:"
<< dirPath;
return;
}
bool encrypted = true;
const QJsonArray encryptedArray =
ServerProfileCrypto::encryptServerArray(array, &encrypted);
if (!encrypted) {
qWarning() << "[ServerManager] refusing to save server config because token encryption failed";
return;
}
const QByteArray payload = QJsonDocument(encryptedArray).toJson();
const QString tmpPath = filePath + QStringLiteral(".tmp");
QFile file(tmpPath);
if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
qWarning() << "[ServerManager] failed to open server config for write:"
<< tmpPath << "|" << file.errorString();
return;
}
if (file.write(payload) != payload.size()) {
qWarning() << "[ServerManager] failed to write complete server config:"
<< tmpPath;
file.close();
QFile::remove(tmpPath);
return;
}
file.close();
QFile::remove(filePath);
if (!QFile::rename(tmpPath, filePath)) {
qWarning() << "[ServerManager] failed to replace server config:"
<< filePath;
QFile::remove(tmpPath);
return;
}
ServerProfileCrypto::setOwnerOnlyPermissions(filePath);
}
void ServerManager::loadSettings() {
QString path = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
QFile file(path + "/servers.json");
QFile file(serversJsonPath());
if (!file.open(QIODevice::ReadOnly)) return;
QJsonArray array = QJsonDocument::fromJson(file.readAll()).array();
bool shouldResave = false;
QJsonArray array = ServerProfileCrypto::decryptServerArray(
QJsonDocument::fromJson(file.readAll()).array(), &shouldResave);
m_servers.clear();
for (auto val : array) {
QJsonObject obj = val.toObject();
@@ -220,6 +269,11 @@ void ServerManager::loadSettings() {
if (!m_servers.isEmpty()) {
setActiveServer(m_servers.first().id);
}
if (shouldResave) {
qInfo() << "[ServerManager] migrating plaintext server tokens to encrypted storage";
saveSettings();
}
}
void ServerManager::clearActiveSession()
@@ -229,4 +283,3 @@ void ServerManager::clearActiveSession()
m_activeClient.reset();
Q_EMIT activeServerChanged(m_activeProfile);
}
@@ -2,6 +2,7 @@
#include "../../api/webdav/webdavclient.h"
#include "../../config/configstore.h"
#include "../../utils/serverprofilecrypto.h"
#include "../../utils/securesecretbox.h"
#include <QDateTime>
@@ -81,7 +82,8 @@ QJsonArray readServersJson()
}
const QByteArray bytes = f.readAll();
f.close();
return QJsonDocument::fromJson(bytes).array();
return ServerProfileCrypto::decryptServerArray(
QJsonDocument::fromJson(bytes).array());
}
bool writeServersJson(const QJsonArray &arr)
@@ -94,7 +96,16 @@ bool writeServersJson(const QJsonArray &arr)
return false;
}
const QByteArray payload = QJsonDocument(arr).toJson(QJsonDocument::Indented);
bool encryptedOk = true;
const QJsonArray encrypted =
ServerProfileCrypto::encryptServerArray(arr, &encryptedOk);
if (!encryptedOk)
{
qWarning() << "[WebdavSyncService] refusing to write servers.json because token encryption failed";
return false;
}
const QByteArray payload =
QJsonDocument(encrypted).toJson(QJsonDocument::Indented);
const QString tmpPath = path + QStringLiteral(".tmp");
{
@@ -124,6 +135,7 @@ bool writeServersJson(const QJsonArray &arr)
}
QFile::remove(tmpPath);
}
ServerProfileCrypto::setOwnerOnlyPermissions(path);
return true;
}
+201
View File
@@ -0,0 +1,201 @@
#include "serverprofilecrypto.h"
#include "securesecretbox.h"
#include <QByteArray>
#include <QDebug>
#include <QFile>
#include <QJsonValue>
namespace
{
constexpr char kAccessToken[] = "accessToken";
constexpr char kAccessTokenEncrypted[] = "accessTokenEncrypted";
QString encryptToken(const QString &plainToken)
{
if (plainToken.isEmpty())
{
return QString();
}
QByteArray tokenBytes = plainToken.toUtf8();
const QByteArray cipher = SecureSecretBox::encryptLocalSecret(tokenBytes);
SecureSecretBox::secureZero(tokenBytes);
if (cipher.isEmpty())
{
return QString();
}
return QString::fromLatin1(cipher.toBase64(QByteArray::Base64Encoding));
}
QString decryptToken(const QString &cipherBase64)
{
if (cipherBase64.isEmpty())
{
return QString();
}
const QByteArray cipher = QByteArray::fromBase64(
cipherBase64.toLatin1(), QByteArray::Base64Encoding);
if (cipher.isEmpty())
{
return QString();
}
auto plain = SecureSecretBox::decryptLocalSecret(cipher);
if (!plain.has_value())
{
return QString();
}
QString token = QString::fromUtf8(plain->constData(), plain->size());
SecureSecretBox::secureZero(*plain);
return token;
}
} // namespace
QJsonObject ServerProfileCrypto::encryptServerObject(QJsonObject obj, bool *ok)
{
if (ok)
{
*ok = true;
}
const QString token =
obj.value(QString::fromLatin1(kAccessToken)).toString();
if (token.isEmpty())
{
obj.remove(QString::fromLatin1(kAccessToken));
obj.remove(QString::fromLatin1(kAccessTokenEncrypted));
return obj;
}
const QString cipher = encryptToken(token);
if (cipher.isEmpty())
{
qWarning() << "[ServerProfileCrypto] failed to encrypt access token"
<< "| serverId:" << obj.value(QStringLiteral("id")).toString();
if (ok)
{
*ok = false;
}
return obj;
}
obj.insert(QString::fromLatin1(kAccessToken), cipher);
obj.insert(QString::fromLatin1(kAccessTokenEncrypted), true);
return obj;
}
QJsonArray ServerProfileCrypto::encryptServerArray(const QJsonArray &array,
bool *ok)
{
bool allOk = true;
QJsonArray encrypted;
for (const QJsonValue &value : array)
{
if (value.isObject())
{
bool objectOk = true;
encrypted.append(encryptServerObject(value.toObject(), &objectOk));
allOk = allOk && objectOk;
}
else
{
encrypted.append(value);
}
}
if (ok)
{
*ok = allOk;
}
return encrypted;
}
QJsonObject ServerProfileCrypto::decryptServerObject(QJsonObject obj,
bool *migrated)
{
if (migrated)
{
*migrated = false;
}
const QString token =
obj.value(QString::fromLatin1(kAccessToken)).toString();
if (token.isEmpty())
{
obj.remove(QString::fromLatin1(kAccessTokenEncrypted));
return obj;
}
const bool encrypted =
obj.value(QString::fromLatin1(kAccessTokenEncrypted)).toBool(false);
if (!encrypted)
{
if (migrated)
{
*migrated = true;
}
return obj;
}
const QString plain = decryptToken(token);
if (plain.isEmpty())
{
qWarning() << "[ServerProfileCrypto] failed to decrypt access token"
<< "| serverId:" << obj.value(QStringLiteral("id")).toString();
obj.remove(QString::fromLatin1(kAccessToken));
obj.remove(QString::fromLatin1(kAccessTokenEncrypted));
return obj;
}
obj.insert(QString::fromLatin1(kAccessToken), plain);
obj.remove(QString::fromLatin1(kAccessTokenEncrypted));
return obj;
}
QJsonArray ServerProfileCrypto::decryptServerArray(const QJsonArray &array,
bool *migrated)
{
bool anyMigrated = false;
QJsonArray decrypted;
for (const QJsonValue &value : array)
{
if (!value.isObject())
{
decrypted.append(value);
continue;
}
bool objectMigrated = false;
decrypted.append(decryptServerObject(value.toObject(), &objectMigrated));
anyMigrated = anyMigrated || objectMigrated;
}
if (migrated)
{
*migrated = anyMigrated;
}
return decrypted;
}
bool ServerProfileCrypto::setOwnerOnlyPermissions(const QString &path)
{
if (path.isEmpty())
{
return false;
}
const bool ok = QFile::setPermissions(
path, QFileDevice::ReadOwner | QFileDevice::WriteOwner);
if (!ok)
{
qWarning() << "[ServerProfileCrypto] failed to set owner-only permissions:"
<< path;
}
return ok;
}
+25
View File
@@ -0,0 +1,25 @@
#ifndef SERVERPROFILECRYPTO_H
#define SERVERPROFILECRYPTO_H
#include "../qEmbyCore_global.h"
#include <QJsonArray>
#include <QJsonObject>
#include <QString>
class QEMBYCORE_EXPORT ServerProfileCrypto
{
public:
static QJsonObject encryptServerObject(QJsonObject obj, bool *ok = nullptr);
static QJsonArray encryptServerArray(const QJsonArray &array, bool *ok = nullptr);
static QJsonObject decryptServerObject(QJsonObject obj, bool *migrated = nullptr);
static QJsonArray decryptServerArray(const QJsonArray &array, bool *migrated = nullptr);
static bool setOwnerOnlyPermissions(const QString &path);
private:
ServerProfileCrypto() = delete;
};
#endif