Offer attachment encryption after selecting a file
This commit is contained in:
1 parent
7e08135bba
commit
b3495349e1
4 files changed
+57
-26
No files matched your search
@@ -1,9 +1,10 @@
|
|||||||
# Зашифрованные вложения группового чата
|
# Зашифрованные вложения группового чата
|
||||||
|
|
||||||
В desktop chatgui кнопка замка рядом со скрепкой включает шифрование следующего
|
В desktop chatgui после выбора файла или перетаскивания в чат рядом с его именем
|
||||||
вложения или голосового сообщения. Выбор фиксируется вместе с адресатом до
|
появляется флажок «Зашифровать вложение». Он применяется к выбранному файлу,
|
||||||
подготовки; переход в другую беседу сбрасывает переключатель. В DM используется
|
изображению или видео. Удаление, замена файла и переход в другую беседу сбрасывают
|
||||||
существующее автоматическое E2E-шифрование лички.
|
флажок. Выбор фиксируется вместе с адресатом при отправке, до подготовки.
|
||||||
|
В DM флажок скрыт: используется существующее автоматическое E2E-шифрование лички.
|
||||||
|
|
||||||
Автор генерирует независимые случайные crypto_UUID и ключ AES-256 на каждую
|
Автор генерирует независимые случайные crypto_UUID и ключ AES-256 на каждую
|
||||||
подготовку. Ключ записывается в локальную SQLite до шифрования. Готовый контейнер
|
подготовку. Ключ записывается в локальную SQLite до шифрования. Готовый контейнер
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QTextEdit>
|
#include <QTextEdit>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
|
#include <QCheckBox>
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
#include <QFileDialog>
|
#include <QFileDialog>
|
||||||
@@ -112,7 +113,16 @@ InputBar::InputBar(EmojiPanel *emojiPanel, AudioRecorder *recorder, QWidget *par
|
|||||||
m_attachPreviewLabel = new QLabel(m_attachPreview);
|
m_attachPreviewLabel = new QLabel(m_attachPreview);
|
||||||
m_attachPreviewLabel->setStyleSheet("background: palette(light); border-radius: 6px; padding: 2px 8px;");
|
m_attachPreviewLabel->setStyleSheet("background: palette(light); border-radius: 6px; padding: 2px 8px;");
|
||||||
prevLay->addWidget(m_attachPreviewLabel, 1);
|
prevLay->addWidget(m_attachPreviewLabel, 1);
|
||||||
|
m_encryptCheck = new QCheckBox(QCoreApplication::translate("ChatGui", "Encrypt attachment"), m_attachPreview);
|
||||||
|
m_encryptCheck->setObjectName("encryptAttachmentCheckBox");
|
||||||
|
m_encryptCheck->setToolTip(QCoreApplication::translate("ChatGui", "Encrypt attachment; approve each recipient's key request"));
|
||||||
|
prevLay->addWidget(m_encryptCheck);
|
||||||
|
connect(m_encryptCheck, &QCheckBox::toggled, this, [this](bool checked) {
|
||||||
|
GUI_DEBUG("attachment encryption choice: target=%s source=%s encrypted=%d",
|
||||||
|
qPrintable(m_channelIdForRecord), qPrintable(m_pendingAttachSrc), checked);
|
||||||
|
});
|
||||||
m_attachClearBtn = new QPushButton(QString::fromUtf8("\xE2\x9C\x95"), m_attachPreview);
|
m_attachClearBtn = new QPushButton(QString::fromUtf8("\xE2\x9C\x95"), m_attachPreview);
|
||||||
|
m_attachClearBtn->setObjectName("clearAttachmentButton");
|
||||||
m_attachClearBtn->setFixedSize(20, 20);
|
m_attachClearBtn->setFixedSize(20, 20);
|
||||||
m_attachClearBtn->setCursor(Qt::PointingHandCursor);
|
m_attachClearBtn->setCursor(Qt::PointingHandCursor);
|
||||||
m_attachClearBtn->setStyleSheet("QPushButton { border: none; background: transparent; color: palette(text); }"
|
m_attachClearBtn->setStyleSheet("QPushButton { border: none; background: transparent; color: palette(text); }"
|
||||||
@@ -133,13 +143,6 @@ InputBar::InputBar(EmojiPanel *emojiPanel, AudioRecorder *recorder, QWidget *par
|
|||||||
QCoreApplication::translate("ChatGui", "Attach"), this);
|
QCoreApplication::translate("ChatGui", "Attach"), this);
|
||||||
connect(m_attachBtn, &QPushButton::clicked, this, &InputBar::onAttachClicked);
|
connect(m_attachBtn, &QPushButton::clicked, this, &InputBar::onAttachClicked);
|
||||||
layout->addWidget(wrapBottomBtn(m_attachBtn, 3), 0, Qt::AlignBottom);
|
layout->addWidget(wrapBottomBtn(m_attachBtn, 3), 0, Qt::AlignBottom);
|
||||||
m_encryptBtn = new QPushButton(QString::fromUtf8("🔒"), this);
|
|
||||||
m_encryptBtn->setObjectName("encryptAttachmentButton");
|
|
||||||
m_encryptBtn->setCheckable(true);
|
|
||||||
m_encryptBtn->setFixedSize(32, 32);
|
|
||||||
m_encryptBtn->setToolTip(QCoreApplication::translate("ChatGui", "Encrypt attachment; approve each recipient's key request"));
|
|
||||||
m_encryptBtn->setAccessibleName(QCoreApplication::translate("ChatGui", "Encrypt attachment"));
|
|
||||||
layout->addWidget(wrapBottomBtn(m_encryptBtn, 3), 0, Qt::AlignBottom);
|
|
||||||
|
|
||||||
m_input->setAcceptRichText(false);
|
m_input->setAcceptRichText(false);
|
||||||
m_input->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
m_input->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||||
@@ -413,7 +416,7 @@ void InputBar::onSendClicked() {
|
|||||||
|
|
||||||
if (hasAttach) {
|
if (hasAttach) {
|
||||||
const quint64 requestId = beginReplySend(reply, m_replyGeneration, m_channelIdForRecord, {}, m_pendingAttachSrc);
|
const quint64 requestId = beginReplySend(reply, m_replyGeneration, m_channelIdForRecord, {}, m_pendingAttachSrc);
|
||||||
const bool encrypted = !m_dmMode && m_encryptBtn->isChecked();
|
const bool encrypted = !m_dmMode && m_encryptCheck->isChecked();
|
||||||
if (encrypted) {
|
if (encrypted) {
|
||||||
auto* request = (struct attachment_send_req*)u_calloc(1, sizeof(struct attachment_send_req));
|
auto* request = (struct attachment_send_req*)u_calloc(1, sizeof(struct attachment_send_req));
|
||||||
QByteArray source = m_pendingAttachSrc.toUtf8(), name = m_pendingAttachName.toUtf8();
|
QByteArray source = m_pendingAttachSrc.toUtf8(), name = m_pendingAttachName.toUtf8();
|
||||||
@@ -434,7 +437,6 @@ void InputBar::onSendClicked() {
|
|||||||
if (reply.size() == 32) memcpy(request->reply_to, reply.constData(), 32);
|
if (reply.size() == 32) memcpy(request->reply_to, reply.constData(), 32);
|
||||||
request->request_id = requestId;
|
request->request_id = requestId;
|
||||||
emit submitAttachment(m_channelIdForRecord, request);
|
emit submitAttachment(m_channelIdForRecord, request);
|
||||||
m_encryptBtn->setChecked(false);
|
|
||||||
} else if (m_pendingAttachAsImage) {
|
} else if (m_pendingAttachAsImage) {
|
||||||
GUI_INFO("send: image=%s name=%s", qPrintable(m_pendingAttachSrc), qPrintable(m_pendingAttachName));
|
GUI_INFO("send: image=%s name=%s", qPrintable(m_pendingAttachSrc), qPrintable(m_pendingAttachName));
|
||||||
emit sendImageAttachment(m_channelIdForRecord, m_pendingAttachSrc, m_pendingAttachDest, m_pendingAttachName, reply, requestId);
|
emit sendImageAttachment(m_channelIdForRecord, m_pendingAttachSrc, m_pendingAttachDest, m_pendingAttachName, reply, requestId);
|
||||||
@@ -471,7 +473,6 @@ void InputBar::setRecordingContext(const QString& mediaDirBase, const QString& c
|
|||||||
if (m_channelIdForRecord != channelId || m_mediaDirBase != mediaDirBase) {
|
if (m_channelIdForRecord != channelId || m_mediaDirBase != mediaDirBase) {
|
||||||
clearReply();
|
clearReply();
|
||||||
++m_attachmentContext;
|
++m_attachmentContext;
|
||||||
m_encryptBtn->setChecked(false);
|
|
||||||
clearPendingAttachment();
|
clearPendingAttachment();
|
||||||
if (m_dropHint) m_dropHint->hide();
|
if (m_dropHint) m_dropHint->hide();
|
||||||
}
|
}
|
||||||
@@ -482,14 +483,14 @@ void InputBar::setRecordingContext(const QString& mediaDirBase, const QString& c
|
|||||||
|
|
||||||
void InputBar::setDmMode(bool enabled) {
|
void InputBar::setDmMode(bool enabled) {
|
||||||
m_dmMode = enabled;
|
m_dmMode = enabled;
|
||||||
m_encryptBtn->setVisible(!enabled);
|
m_encryptCheck->setVisible(!enabled);
|
||||||
if (enabled) m_encryptBtn->setChecked(false);
|
if (enabled) m_encryptCheck->setChecked(false);
|
||||||
m_pttBtn->setEnabled(true);
|
m_pttBtn->setEnabled(true);
|
||||||
m_attachBtn->setEnabled(true);
|
m_attachBtn->setEnabled(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void InputBar::setMediaEnabled(bool enabled) {
|
void InputBar::setMediaEnabled(bool enabled) {
|
||||||
if (m_encryptBtn) m_encryptBtn->setEnabled(enabled);
|
if (m_encryptCheck) m_encryptCheck->setEnabled(enabled);
|
||||||
if (m_pttBtn) m_pttBtn->setEnabled(enabled);
|
if (m_pttBtn) m_pttBtn->setEnabled(enabled);
|
||||||
if (m_attachBtn) m_attachBtn->setEnabled(enabled);
|
if (m_attachBtn) m_attachBtn->setEnabled(enabled);
|
||||||
if (!enabled && m_dropHint) m_dropHint->hide();
|
if (!enabled && m_dropHint) m_dropHint->hide();
|
||||||
@@ -502,7 +503,6 @@ void InputBar::onPttPressed() {
|
|||||||
m_pttActive = m_recorder->isRecording();
|
m_pttActive = m_recorder->isRecording();
|
||||||
if (!m_pttActive) { DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "voice recording did not start; composer remains idle"); return; }
|
if (!m_pttActive) { DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "voice recording did not start; composer remains idle"); return; }
|
||||||
m_recordTarget = m_channelIdForRecord;
|
m_recordTarget = m_channelIdForRecord;
|
||||||
m_recordEncrypted = !m_dmMode && m_encryptBtn->isChecked();
|
|
||||||
m_recordReply = m_replyTo; m_recordReplyGeneration = m_replyGeneration; m_replyFailed = false;
|
m_recordReply = m_replyTo; m_recordReplyGeneration = m_replyGeneration; m_replyFailed = false;
|
||||||
m_pttBtn->setStyleSheet(m_pttBtn->styleSheet() +
|
m_pttBtn->setStyleSheet(m_pttBtn->styleSheet() +
|
||||||
"QPushButton { background: #e74c3c; border-radius: 4px; }");
|
"QPushButton { background: #e74c3c; border-radius: 4px; }");
|
||||||
@@ -525,12 +525,10 @@ void InputBar::onPttReleased() {
|
|||||||
auto* request = (struct attachment_send_req*)u_calloc(1, sizeof(struct attachment_send_req));
|
auto* request = (struct attachment_send_req*)u_calloc(1, sizeof(struct attachment_send_req));
|
||||||
if (!request) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "InputBar: voice request allocation failed"); return; }
|
if (!request) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "InputBar: voice request allocation failed"); return; }
|
||||||
request->info.kind = ATTACHMENT_VOICE; request->preset = VoiceEncoder::preset();
|
request->info.kind = ATTACHMENT_VOICE; request->preset = VoiceEncoder::preset();
|
||||||
request->encrypted = m_recordEncrypted;
|
|
||||||
if (!m_recorder->takeRecording(request)) { u_free(request); return; }
|
if (!m_recorder->takeRecording(request)) { u_free(request); return; }
|
||||||
if (m_recordReply.size() == 32) memcpy(request->reply_to, m_recordReply.constData(), 32);
|
if (m_recordReply.size() == 32) memcpy(request->reply_to, m_recordReply.constData(), 32);
|
||||||
request->request_id = beginReplySend(m_recordReply, m_recordReplyGeneration, m_recordTarget);
|
request->request_id = beginReplySend(m_recordReply, m_recordReplyGeneration, m_recordTarget);
|
||||||
emit submitAttachment(m_recordTarget, request);
|
emit submitAttachment(m_recordTarget, request);
|
||||||
if (m_recordTarget == m_channelIdForRecord) m_encryptBtn->setChecked(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void InputBar::onAttachClicked() {
|
void InputBar::onAttachClicked() {
|
||||||
@@ -595,6 +593,7 @@ bool InputBar::prepareAttachment(const QString& srcPath, quint64 context) {
|
|||||||
.arg(fi.suffix().isEmpty() ? "bin" : fi.suffix());
|
.arg(fi.suffix().isEmpty() ? "bin" : fi.suffix());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m_encryptCheck->setChecked(false);
|
||||||
m_pendingAttachSrc = srcPath;
|
m_pendingAttachSrc = srcPath;
|
||||||
++m_attachmentRevision;
|
++m_attachmentRevision;
|
||||||
m_pendingAttachDest = destPath;
|
m_pendingAttachDest = destPath;
|
||||||
@@ -615,6 +614,7 @@ bool InputBar::prepareAttachment(const QString& srcPath, quint64 context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void InputBar::clearPendingAttachment() {
|
void InputBar::clearPendingAttachment() {
|
||||||
|
m_encryptCheck->setChecked(false);
|
||||||
++m_attachmentRevision;
|
++m_attachmentRevision;
|
||||||
m_pendingAttachSrc.clear();
|
m_pendingAttachSrc.clear();
|
||||||
m_pendingAttachDest.clear();
|
m_pendingAttachDest.clear();
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
class QTextEdit;
|
class QTextEdit;
|
||||||
class QPushButton;
|
class QPushButton;
|
||||||
|
class QCheckBox;
|
||||||
class QLabel;
|
class QLabel;
|
||||||
class QProgressBar;
|
class QProgressBar;
|
||||||
class QTimer;
|
class QTimer;
|
||||||
@@ -83,8 +84,7 @@ private:
|
|||||||
QPushButton *m_emojiBtn;
|
QPushButton *m_emojiBtn;
|
||||||
QPushButton *m_pttBtn;
|
QPushButton *m_pttBtn;
|
||||||
QPushButton *m_attachBtn = nullptr;
|
QPushButton *m_attachBtn = nullptr;
|
||||||
QPushButton *m_encryptBtn = nullptr;
|
QCheckBox *m_encryptCheck = nullptr;
|
||||||
bool m_recordEncrypted = false;
|
|
||||||
EmojiPanel *m_emojiPanel;
|
EmojiPanel *m_emojiPanel;
|
||||||
AudioRecorder *m_recorder;
|
AudioRecorder *m_recorder;
|
||||||
QWidget *m_recordOverlay = nullptr;
|
QWidget *m_recordOverlay = nullptr;
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ extern "C" {
|
|||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
#include <QAbstractButton>
|
#include <QAbstractButton>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
|
#include <QCheckBox>
|
||||||
|
|
||||||
static QString makeFile(const QTemporaryDir& dir, const QString& name, const QByteArray& contents = "test") {
|
static QString makeFile(const QTemporaryDir& dir, const QString& name, const QByteArray& contents = "test") {
|
||||||
const QString path = dir.filePath(name);
|
const QString path = dir.filePath(name);
|
||||||
@@ -47,12 +48,19 @@ static bool dropUrls(QWidget* target, const QList<QUrl>& urls, bool process = tr
|
|||||||
class FileDropTest : public QObject {
|
class FileDropTest : public QObject {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
private slots:
|
private slots:
|
||||||
|
void encryptedAttachmentCapturesTargetAndResetsChoice_data() {
|
||||||
|
QTest::addColumn<bool>("picker");
|
||||||
|
QTest::newRow("drop") << false; QTest::newRow("file-picker") << true;
|
||||||
|
}
|
||||||
void encryptedAttachmentCapturesTargetAndResetsChoice() {
|
void encryptedAttachmentCapturesTargetAndResetsChoice() {
|
||||||
|
QFETCH(bool, picker);
|
||||||
QTemporaryDir dir; QVERIFY(dir.isValid());
|
QTemporaryDir dir; QVERIFY(dir.isValid());
|
||||||
const QString file = makeFile(dir, "private.txt", "private bytes");
|
const QString file = makeFile(dir, "private.txt", "private bytes");
|
||||||
InputBar bar(nullptr, nullptr); bar.show(); bar.setRecordingContext(dir.filePath("media"), "123");
|
InputBar bar(nullptr, nullptr); bar.show(); bar.setRecordingContext(dir.filePath("media"), "123");
|
||||||
auto* editor = bar.findChild<QTextEdit*>(); QVERIFY(editor);
|
auto* editor = bar.findChild<QTextEdit*>(); QVERIFY(editor);
|
||||||
auto* encrypt = bar.findChild<QPushButton*>("encryptAttachmentButton"); QVERIFY(encrypt);
|
auto* encrypt = bar.findChild<QCheckBox*>("encryptAttachmentCheckBox"); QVERIFY(encrypt);
|
||||||
|
auto* clear = bar.findChild<QPushButton*>("clearAttachmentButton"); QVERIFY(clear);
|
||||||
|
QVERIFY(!encrypt->isVisible()); QVERIFY(!encrypt->isChecked());
|
||||||
QSignalSpy ordinary(&bar, &InputBar::sendFileAttachment);
|
QSignalSpy ordinary(&bar, &InputBar::sendFileAttachment);
|
||||||
int encrypted = 0;
|
int encrypted = 0;
|
||||||
connect(&bar, &InputBar::submitAttachment, &bar, [&](const QString& target, attachment_send_req* request) {
|
connect(&bar, &InputBar::submitAttachment, &bar, [&](const QString& target, attachment_send_req* request) {
|
||||||
@@ -60,13 +68,35 @@ private slots:
|
|||||||
QCOMPARE(QString::fromUtf8(request->source), file); QCOMPARE(QString::fromUtf8(request->info.name), QString("private.txt"));
|
QCOMPARE(QString::fromUtf8(request->source), file); QCOMPARE(QString::fromUtf8(request->info.name), QString("private.txt"));
|
||||||
++encrypted; u_free(request);
|
++encrypted; u_free(request);
|
||||||
});
|
});
|
||||||
QVERIFY(dropUrls(editor->viewport(), {QUrl::fromLocalFile(file)}));
|
QTimer timer; timer.setInterval(1);
|
||||||
|
if (picker) {
|
||||||
|
connect(&timer, &QTimer::timeout, &bar, [&] {
|
||||||
|
auto* dialog = qobject_cast<QFileDialog*>(QApplication::activeModalWidget());
|
||||||
|
if (!dialog) return;
|
||||||
|
timer.stop(); dialog->selectFile(file);
|
||||||
|
QMetaObject::invokeMethod(dialog, "accept", Qt::DirectConnection);
|
||||||
|
});
|
||||||
|
timer.start(); QVERIFY(QMetaObject::invokeMethod(&bar, "onAttachClicked", Qt::DirectConnection));
|
||||||
|
} else QVERIFY(dropUrls(editor->viewport(), {QUrl::fromLocalFile(file)}));
|
||||||
|
QVERIFY(encrypt->isVisible()); QVERIFY(!encrypt->isChecked());
|
||||||
QTest::mouseClick(encrypt, Qt::LeftButton); QVERIFY(encrypt->isChecked());
|
QTest::mouseClick(encrypt, Qt::LeftButton); QVERIFY(encrypt->isChecked());
|
||||||
QTest::keyClick(editor, Qt::Key_Return);
|
QTest::keyClick(editor, Qt::Key_Return);
|
||||||
QCOMPARE(encrypted, 1); QCOMPARE(ordinary.size(), 0); QVERIFY(!encrypt->isChecked()); QVERIFY(QFile::exists(file));
|
QCOMPARE(encrypted, 1); QCOMPARE(ordinary.size(), 0); QVERIFY(!encrypt->isChecked()); QVERIFY(QFile::exists(file));
|
||||||
|
QVERIFY(!encrypt->isVisible());
|
||||||
|
QVERIFY(dropUrls(editor->viewport(), {QUrl::fromLocalFile(file)}));
|
||||||
QTest::mouseClick(encrypt, Qt::LeftButton); QVERIFY(encrypt->isChecked());
|
QTest::mouseClick(encrypt, Qt::LeftButton); QVERIFY(encrypt->isChecked());
|
||||||
bar.setRecordingContext(dir.filePath("media"), "456"); QVERIFY(!encrypt->isChecked());
|
QTest::mouseClick(clear, Qt::LeftButton); QVERIFY(!encrypt->isVisible()); QVERIFY(!encrypt->isChecked());
|
||||||
bar.setDmMode(true); QVERIFY(encrypt->isHidden()); QVERIFY(!encrypt->isChecked());
|
QVERIFY(dropUrls(editor->viewport(), {QUrl::fromLocalFile(file)}));
|
||||||
|
QTest::mouseClick(encrypt, Qt::LeftButton); QVERIFY(encrypt->isChecked());
|
||||||
|
QVERIFY(dropUrls(editor->viewport(), {QUrl::fromLocalFile(dir.filePath("missing.txt"))}));
|
||||||
|
QVERIFY(encrypt->isChecked());
|
||||||
|
QVERIFY(dropUrls(editor->viewport(), {QUrl::fromLocalFile(makeFile(dir, "second.txt"))}));
|
||||||
|
QVERIFY(encrypt->isVisible()); QVERIFY(!encrypt->isChecked());
|
||||||
|
QTest::mouseClick(encrypt, Qt::LeftButton); QVERIFY(encrypt->isChecked());
|
||||||
|
bar.setRecordingContext(dir.filePath("media"), "456"); QVERIFY(!encrypt->isVisible()); QVERIFY(!encrypt->isChecked());
|
||||||
|
bar.setDmMode(true);
|
||||||
|
QVERIFY(dropUrls(editor->viewport(), {QUrl::fromLocalFile(file)}));
|
||||||
|
QVERIFY(!encrypt->isVisible()); QVERIFY(!encrypt->isChecked());
|
||||||
}
|
}
|
||||||
|
|
||||||
void replyDraftAndLateResults() {
|
void replyDraftAndLateResults() {
|
||||||
|
|||||||
Reference in new issue
Block a user