Skip to content

Commit

Permalink
Allow processing clipboard containing "secrets"
Browse files Browse the repository at this point in the history
This allows processing the "secret" clipboard data by overriding new
`onSecretClipboardChanged()` script function.

This also ensures that callbacks are called consistently with properly
set formats `mimeClipboardMode`, `mimeOutputTab` and `mimeCurrentTab`.

Fixes #2787
  • Loading branch information
hluk committed Jul 30, 2024
1 parent 5751cdf commit 81432fc
Show file tree
Hide file tree
Showing 9 changed files with 92 additions and 65 deletions.
17 changes: 15 additions & 2 deletions docs/scripting-api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1496,6 +1496,21 @@ unlike in GUI, where row numbers start from 1 by default.

Default implementation calls :js:func:`updateClipboardData`.

.. js:function:: onSecretClipboardChanged()

Called if the clipboard or `Linux mouse selection`_ changes and contains a
password or other secret (for example, copied from clipboard manager).

The default implementation clears all data, so they are not accessible using
:js:func:`data` and :js:func:`dataFormats`, except :js:data:`mimeSecret`,
and calls :js:func:`updateClipboardData`.

**Be careful overriding** this function (via a Script command). Calling
`onClipboardChanged()` without clearing the data and without any further
checks can cause storing and processing secrets from password managers. On
the other hand, it can help to get access to the data copied, for example
from a web browser in private mode.

.. js:function:: onClipboardUnchanged()

Called when clipboard or `Linux mouse selection`_ changes but data remained the same.
Expand Down Expand Up @@ -2212,8 +2227,6 @@ These MIME types values are assigned to global variables prefixed with

If set to ``1``, the clipboard contains a password or other secret (for example, copied from clipboard manager).

In such case, the data won't be available in the app, not even via calling ``data()`` script function.

.. js:data:: mimeShortcut

Application or global shortcut which activated the command. Value: 'application/x-copyq-shortcut'.
Expand Down
77 changes: 40 additions & 37 deletions src/app/clipboardmonitor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ bool isClipboardDataHidden(const QVariantMap &data)
return data.value(mimeHidden).toByteArray() == "1";
}

bool isClipboardDataSecret(const QVariantMap &data)
{
return data.value(mimeSecret).toByteArray() == "1";
}

int defaultOwnerUpdateInterval()
{
#ifdef COPYQ_WS_X11
Expand Down Expand Up @@ -79,7 +84,8 @@ ClipboardMonitor::ClipboardMonitor(const QStringList &formats)
m_storeSelection = config.option<Config::check_selection>();
m_runSelection = config.option<Config::run_selection>();

m_clipboardToSelection = config.option<Config::copy_clipboard>();
m_clipboardToSelection = config.option<Config::copy_clipboard>()
&& m_clipboard->isSelectionSupported();
m_selectionToClipboard = config.option<Config::copy_selection>();

if (!m_storeSelection && !m_runSelection && !m_selectionToClipboard) {
Expand Down Expand Up @@ -122,19 +128,15 @@ void ClipboardMonitor::onClipboardChanged(ClipboardMode mode)
auto clipboardData = mode == ClipboardMode::Clipboard
? &m_clipboardData : &m_selectionData;

if ( hasSameData(data, *clipboardData) ) {
const bool isDataUnchanged = hasSameData(data, *clipboardData);
if (!isDataUnchanged) {
*clipboardData = data;
#ifdef HAS_MOUSE_SELECTIONS
if ( !m_runSelection && mode == ClipboardMode::Selection )
return;
#endif
COPYQ_LOG( QString("Ignoring unchanged %1")
.arg(mode == ClipboardMode::Clipboard ? "clipboard" : "selection") );
emit clipboardUnchanged(data);
} else if (isDataUnchanged && !m_runSelection && mode == ClipboardMode::Selection) {
return;
#endif
}

*clipboardData = data;

if ( !data.contains(mimeOwner)
&& !data.contains(mimeWindowTitle)
&& !m_clipboardOwner.isEmpty() )
Expand All @@ -143,12 +145,33 @@ void ClipboardMonitor::onClipboardChanged(ClipboardMode mode)
}

COPYQ_LOG( QString("%1 changed, owner is \"%2\"")
.arg(mode == ClipboardMode::Clipboard ? "Clipboard" : "Selection",
.arg(QLatin1String(mode == ClipboardMode::Clipboard ? "Clipboard" : "Selection"),
getTextData(data, mimeOwner)) );

const auto defaultTab = m_clipboardTab.isEmpty() ? defaultClipboardTabName() : m_clipboardTab;
setTextData(&data, defaultTab, mimeCurrentTab);

#ifdef HAS_MOUSE_SELECTIONS
if (mode == ClipboardMode::Selection)
data.insert(mimeClipboardMode, QByteArrayLiteral("selection"));

if (mode == ClipboardMode::Clipboard ? m_storeClipboard : m_storeSelection) {
#else
if (m_storeClipboard) {
#endif
setTextData(&data, m_clipboardTab, mimeOutputTab);
}

// if data themselves did not change, call just clipboardUnchanged()
if (isDataUnchanged) {
COPYQ_LOG( QStringLiteral("Ignoring unchanged %1")
.arg(QLatin1String(mode == ClipboardMode::Clipboard ? "clipboard" : "selection")) );
emit clipboardUnchanged(data);
return;
}

#ifdef HAS_MOUSE_SELECTIONS
if ( (mode == ClipboardMode::Clipboard ? m_clipboardToSelection : m_selectionToClipboard)
&& m_clipboard->isSelectionSupported()
&& !data.contains(mimeOwner) )
{
const auto text = getTextData(data);
Expand All @@ -165,40 +188,20 @@ void ClipboardMonitor::onClipboardChanged(ClipboardMode mode)

// omit running run automatic commands when disabled
if ( !m_runSelection && mode == ClipboardMode::Selection ) {
if ( m_storeSelection && !m_clipboardTab.isEmpty() ) {
data.insert(mimeClipboardMode, QByteArrayLiteral("selection"));
setTextData(&data, m_clipboardTab, mimeOutputTab);
if ( m_storeSelection && !m_clipboardTab.isEmpty() && !isClipboardDataSecret(data) )
emit saveData(data);
}
return;
}
#endif

if (mode != ClipboardMode::Clipboard) {
const QString modeName = mode == ClipboardMode::Selection
? QStringLiteral("selection")
: QStringLiteral("find buffer");
data.insert(mimeClipboardMode, modeName);
}

// run automatic commands
if ( anySessionOwnsClipboardData(data) ) {
// run script callbacks
if ( isClipboardDataSecret(data) ) {
emit secretClipboardChanged(data);
} else if ( anySessionOwnsClipboardData(data) ) {
emit clipboardChanged(data, ClipboardOwnership::Own);
} else if ( isClipboardDataHidden(data) ) {
emit clipboardChanged(data, ClipboardOwnership::Hidden);
} else {
const auto defaultTab = m_clipboardTab.isEmpty() ? defaultClipboardTabName() : m_clipboardTab;
setTextData(&data, defaultTab, mimeCurrentTab);


#ifdef HAS_MOUSE_SELECTIONS
if (mode == ClipboardMode::Clipboard ? m_storeClipboard : m_storeSelection) {
#else
if (m_storeClipboard) {
#endif
setTextData(&data, m_clipboardTab, mimeOutputTab);
}

emit clipboardChanged(data, ClipboardOwnership::Foreign);
}
}
1 change: 1 addition & 0 deletions src/app/clipboardmonitor.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class ClipboardMonitor final : public QObject

signals:
void clipboardChanged(const QVariantMap &data, ClipboardOwnership ownership);
void secretClipboardChanged(const QVariantMap &data);
void clipboardUnchanged(const QVariantMap &data);
void saveData(const QVariantMap &data);
void synchronizeSelection(ClipboardMode sourceMode, uint sourceTextHash, uint targetTextHash);
Expand Down
1 change: 1 addition & 0 deletions src/gui/commandcompleterdocumentation.h
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ void addDocumentation(AddDocumentationCallback addDocumentation)
addDocumentation("onClipboardChanged", "onClipboardChanged()", "Called when clipboard or `Linux mouse selection`_ changes.");
addDocumentation("onOwnClipboardChanged", "onOwnClipboardChanged()", "Called when clipboard or `Linux mouse selection`_ changes by a CopyQ instance.");
addDocumentation("onHiddenClipboardChanged", "onHiddenClipboardChanged()", "Called when hidden clipboard or `Linux mouse selection`_ changes.");
addDocumentation("onSecretClipboardChanged", "onSecretClipboardChanged()", "Called if the clipboard or `Linux mouse selection`_ changes and contains a password or other secret (for example, copied from clipboard manager).");
addDocumentation("onClipboardUnchanged", "onClipboardUnchanged()", "Called when clipboard or `Linux mouse selection`_ changes but data remained the same.");
addDocumentation("onStart", "onStart()", "Called when application starts.");
addDocumentation("onExit", "onExit()", "Called just before application exists.");
Expand Down
27 changes: 9 additions & 18 deletions src/platform/dummy/dummyclipboard.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,6 @@
#include <QMimeData>
#include <QStringList>

namespace {

const QMimeData *createSecretData()
{
auto data = new QMimeData();
data->setData(mimeSecret, QByteArrayLiteral("1"));
return data;
}

} // namespace

QClipboard::Mode modeToQClipboardMode(ClipboardMode mode)
{
switch (mode) {
Expand All @@ -43,7 +32,15 @@ void DummyClipboard::startMonitoring(const QStringList &)
QVariantMap DummyClipboard::data(ClipboardMode mode, const QStringList &formats) const
{
const QMimeData *data = mimeData(mode);
return data ? cloneData(*data, formats) : QVariantMap();
if (data == nullptr)
return {};

const bool isDataSecret = isHidden(*data);
QVariantMap dataMap = cloneData(*data, formats);
if (isDataSecret)
dataMap[mimeSecret] = QByteArrayLiteral("1");

return dataMap;
}

void DummyClipboard::setData(ClipboardMode mode, const QVariantMap &dataMap)
Expand All @@ -68,12 +65,6 @@ const QMimeData *DummyClipboard::mimeData(ClipboardMode mode) const
return nullptr;
}

if (isHidden(*data)) {
log( QStringLiteral("Hiding secret %1 data").arg(modeText) );
static const QMimeData *secretData = createSecretData();
return secretData;
}

COPYQ_LOG_VERBOSE( QStringLiteral("Got %1 data").arg(modeText) );
return data;
}
Expand Down
8 changes: 6 additions & 2 deletions src/platform/x11/x11platformclipboard.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

#include <QClipboard>
#include <QMimeData>
#include <QPointer>

namespace {

Expand Down Expand Up @@ -245,7 +246,7 @@ void X11PlatformClipboard::updateClipboardData(X11PlatformClipboard::ClipboardDa
return;
}

const auto data = mimeData(clipboardData->mode);
const QPointer<const QMimeData> data( mimeData(clipboardData->mode) );

// Retry to retrieve clipboard data few times.
if (!data) {
Expand Down Expand Up @@ -283,14 +284,17 @@ void X11PlatformClipboard::updateClipboardData(X11PlatformClipboard::ClipboardDa
// text did not change.
if ( newDataTimestamp != 0 && clipboardData->newDataTimestamp == newDataTimestamp ) {
const QVariantMap newData = cloneData(*data, {mimeText});
if (newData.value(mimeText) == clipboardData->newData.value(mimeText))
if (!data || newData.value(mimeText) == clipboardData->newData.value(mimeText))
return;
}

clipboardData->timerEmitChange.stop();
clipboardData->abortCloning = false;
clipboardData->cloningData = true;
const bool isDataSecret = isHidden(*data);
clipboardData->newData = cloneData(*data, clipboardData->formats, &clipboardData->abortCloning);
if (isDataSecret)
clipboardData->newData[mimeSecret] = QByteArrayLiteral("1");
clipboardData->cloningData = false;
if (clipboardData->abortCloning) {
m_timerCheckAgain.setInterval(0);
Expand Down
14 changes: 14 additions & 0 deletions src/scriptable/scriptable.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2382,6 +2382,13 @@ void Scriptable::onClipboardUnchanged()
{
}

void Scriptable::onSecretClipboardChanged()
{
// Drop secret data by default
m_data = {{mimeSecret, m_data.value(mimeSecret)}};
eval("updateClipboardData()");
}

void Scriptable::synchronizeToSelection()
{
if ( canSynchronizeSelection(ClipboardMode::Selection) ) {
Expand Down Expand Up @@ -2605,6 +2612,8 @@ void Scriptable::monitorClipboard()
connect(this, &Scriptable::finished, &loop, &QEventLoop::quit);
connect( &monitor, &ClipboardMonitor::clipboardChanged,
this, &Scriptable::onMonitorClipboardChanged );
connect( &monitor, &ClipboardMonitor::secretClipboardChanged,
this, &Scriptable::onMonitorSecretClipboardChanged );
connect( &monitor, &ClipboardMonitor::clipboardUnchanged,
this, &Scriptable::onMonitorClipboardUnchanged );
connect( &monitor, &ClipboardMonitor::synchronizeSelection,
Expand Down Expand Up @@ -2704,6 +2713,11 @@ void Scriptable::onMonitorClipboardChanged(const QVariantMap &data, ClipboardOwn
m_proxy->runInternalAction(data, command);
}

void Scriptable::onMonitorSecretClipboardChanged(const QVariantMap &data)
{
m_proxy->runInternalAction(data, QStringLiteral("copyq onSecretClipboardChanged"));
}

void Scriptable::onMonitorClipboardUnchanged(const QVariantMap &data)
{
m_proxy->runInternalAction(data, "copyq onClipboardUnchanged");
Expand Down
2 changes: 2 additions & 0 deletions src/scriptable/scriptable.h
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,7 @@ public slots:
void onOwnClipboardChanged();
void onHiddenClipboardChanged();
void onClipboardUnchanged();
void onSecretClipboardChanged();

void onStart() {}
void onExit() {}
Expand Down Expand Up @@ -406,6 +407,7 @@ public slots:

private:
void onMonitorClipboardChanged(const QVariantMap &data, ClipboardOwnership ownership);
void onMonitorSecretClipboardChanged(const QVariantMap &data);
void onMonitorClipboardUnchanged(const QVariantMap &data);
void onSynchronizeSelection(ClipboardMode sourceMode, uint sourceTextHash, uint targetTextHash);
void onFetchCurrentClipboardOwner(QString *title);
Expand Down
10 changes: 4 additions & 6 deletions src/tests/tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4854,14 +4854,13 @@ void Tests::startServerAndRunCommand()
void Tests::avoidStoringPasswords()
{
TEST( m_test->setClipboard(secretData("secret")) );
waitFor(2 * waitMsPasteClipboard);
RUN("clipboard" << "?", mimeSecret + "\n");
WAIT_ON_OUTPUT("clipboard", "secret");
RUN("read" << "0" << "1" << "2", "\n\n");
RUN("count", "0\n");

RUN("keys" << clipboardBrowserId << keyNameFor(QKeySequence::Paste), "");
waitFor(waitMsPasteClipboard);
RUN("read" << "0" << "1" << "2", "\n\n");
RUN("read" << "0" << "1" << "2", "secret\n\n");
RUN("count", "1\n");
}

Expand All @@ -4870,15 +4869,14 @@ void Tests::scriptsForPasswords()
const auto script = R"(
setCommands([{
isScript: true,
cmd: `global.updateClipboardData = function() {
if (data(mimeSecret) == "1") add("SECRET");
cmd: `global.onSecretClipboardChanged = function() {
add("SECRET");
}`
}])
)";
RUN(script, "");
WAIT_ON_OUTPUT("commands().length", "1\n");
TEST( m_test->setClipboard(secretData("secret")) );
waitFor(2 * waitMsPasteClipboard);
WAIT_ON_OUTPUT("read" << "0" << "1" << "2", "SECRET\n\n");
RUN("count", "1\n");
}
Expand Down

0 comments on commit 81432fc

Please sign in to comment.