feat: encrypt saved server profile tokens
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user