feat: encrypt saved server profile tokens
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
#include "configbundle.h"
|
#include "configbundle.h"
|
||||||
|
|
||||||
#include "../../config/configstore.h"
|
#include "../../config/configstore.h"
|
||||||
|
#include "../../utils/serverprofilecrypto.h"
|
||||||
|
|
||||||
#include <QCoreApplication>
|
#include <QCoreApplication>
|
||||||
#include <QDateTime>
|
#include <QDateTime>
|
||||||
@@ -304,7 +305,9 @@ ConfigBundle ConfigBundle::collectFromLocal()
|
|||||||
f.close();
|
f.close();
|
||||||
const QJsonDocument doc = QJsonDocument::fromJson(bytes);
|
const QJsonDocument doc = QJsonDocument::fromJson(bytes);
|
||||||
if (doc.isArray()) {
|
if (doc.isArray()) {
|
||||||
b.servers = doc.array();
|
bool migrated = false;
|
||||||
|
b.servers =
|
||||||
|
ServerProfileCrypto::decryptServerArray(doc.array(), &migrated);
|
||||||
} else {
|
} else {
|
||||||
qWarning() << "[ConfigBundle] servers.json is not array, skipped";
|
qWarning() << "[ConfigBundle] servers.json is not array, skipped";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,25 @@
|
|||||||
#include "servermanager.h"
|
#include "servermanager.h"
|
||||||
#include "../../api/embywebsocket.h"
|
#include "../../api/embywebsocket.h"
|
||||||
|
#include "../../utils/serverprofilecrypto.h"
|
||||||
|
#include <QDebug>
|
||||||
#include <QJsonArray>
|
#include <QJsonArray>
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
|
#include <QFileInfo>
|
||||||
#include <QStandardPaths>
|
#include <QStandardPaths>
|
||||||
#include <QDir>
|
#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)
|
ServerManager::ServerManager(NetworkManager* nm, QObject* parent)
|
||||||
: QObject(parent), m_network(nm) {
|
: QObject(parent), m_network(nm) {
|
||||||
loadSettings();
|
loadSettings();
|
||||||
@@ -181,20 +195,55 @@ void ServerManager::saveSettings() {
|
|||||||
array.append(obj);
|
array.append(obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
QString path = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
|
const QString filePath = serversJsonPath();
|
||||||
QDir().mkpath(path);
|
const QString dirPath = QFileInfo(filePath).absolutePath();
|
||||||
QFile file(path + "/servers.json");
|
if (!QDir().mkpath(dirPath)) {
|
||||||
if (file.open(QIODevice::WriteOnly)) {
|
qWarning() << "[ServerManager] failed to create server config dir:"
|
||||||
file.write(QJsonDocument(array).toJson());
|
<< 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() {
|
void ServerManager::loadSettings() {
|
||||||
QString path = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
|
QFile file(serversJsonPath());
|
||||||
QFile file(path + "/servers.json");
|
|
||||||
if (!file.open(QIODevice::ReadOnly)) return;
|
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();
|
m_servers.clear();
|
||||||
for (auto val : array) {
|
for (auto val : array) {
|
||||||
QJsonObject obj = val.toObject();
|
QJsonObject obj = val.toObject();
|
||||||
@@ -220,6 +269,11 @@ void ServerManager::loadSettings() {
|
|||||||
if (!m_servers.isEmpty()) {
|
if (!m_servers.isEmpty()) {
|
||||||
setActiveServer(m_servers.first().id);
|
setActiveServer(m_servers.first().id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (shouldResave) {
|
||||||
|
qInfo() << "[ServerManager] migrating plaintext server tokens to encrypted storage";
|
||||||
|
saveSettings();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ServerManager::clearActiveSession()
|
void ServerManager::clearActiveSession()
|
||||||
@@ -229,4 +283,3 @@ void ServerManager::clearActiveSession()
|
|||||||
m_activeClient.reset();
|
m_activeClient.reset();
|
||||||
Q_EMIT activeServerChanged(m_activeProfile);
|
Q_EMIT activeServerChanged(m_activeProfile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include "../../api/webdav/webdavclient.h"
|
#include "../../api/webdav/webdavclient.h"
|
||||||
#include "../../config/configstore.h"
|
#include "../../config/configstore.h"
|
||||||
|
#include "../../utils/serverprofilecrypto.h"
|
||||||
#include "../../utils/securesecretbox.h"
|
#include "../../utils/securesecretbox.h"
|
||||||
|
|
||||||
#include <QDateTime>
|
#include <QDateTime>
|
||||||
@@ -81,7 +82,8 @@ QJsonArray readServersJson()
|
|||||||
}
|
}
|
||||||
const QByteArray bytes = f.readAll();
|
const QByteArray bytes = f.readAll();
|
||||||
f.close();
|
f.close();
|
||||||
return QJsonDocument::fromJson(bytes).array();
|
return ServerProfileCrypto::decryptServerArray(
|
||||||
|
QJsonDocument::fromJson(bytes).array());
|
||||||
}
|
}
|
||||||
|
|
||||||
bool writeServersJson(const QJsonArray &arr)
|
bool writeServersJson(const QJsonArray &arr)
|
||||||
@@ -94,7 +96,16 @@ bool writeServersJson(const QJsonArray &arr)
|
|||||||
return false;
|
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");
|
const QString tmpPath = path + QStringLiteral(".tmp");
|
||||||
{
|
{
|
||||||
@@ -124,6 +135,7 @@ bool writeServersJson(const QJsonArray &arr)
|
|||||||
}
|
}
|
||||||
QFile::remove(tmpPath);
|
QFile::remove(tmpPath);
|
||||||
}
|
}
|
||||||
|
ServerProfileCrypto::setOwnerOnlyPermissions(path);
|
||||||
return true;
|
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