You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
892 lines
26 KiB
892 lines
26 KiB
/*
|
|
Copyright (c) 2014, Lukas Holecek <hluk@email.cz>
|
|
|
|
This file is part of CopyQ.
|
|
|
|
CopyQ is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
CopyQ is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with CopyQ. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "itemencrypted.h"
|
|
#include "ui_itemencryptedsettings.h"
|
|
|
|
#include "common/command.h"
|
|
#include "common/config.h"
|
|
#include "common/contenttype.h"
|
|
#include "common/log.h"
|
|
#include "common/mimetypes.h"
|
|
#include "common/shortcuts.h"
|
|
#include "common/textdata.h"
|
|
#include "gui/icons.h"
|
|
#include "gui/iconwidget.h"
|
|
#include "item/serialize.h"
|
|
|
|
#ifdef HAS_TESTS
|
|
# include "tests/itemencryptedtests.h"
|
|
#endif
|
|
|
|
#include <QDir>
|
|
#include <QIODevice>
|
|
#include <QLabel>
|
|
#include <QTextEdit>
|
|
#include <QtPlugin>
|
|
#include <QVBoxLayout>
|
|
|
|
namespace {
|
|
|
|
const char mimeEncryptedData[] = "application/x-copyq-encrypted";
|
|
|
|
const char dataFileHeader[] = "CopyQ_encrypted_tab";
|
|
const char dataFileHeaderV2[] = "CopyQ_encrypted_tab v2";
|
|
|
|
const int maxItemCount = 10000;
|
|
|
|
struct KeyPairPaths {
|
|
KeyPairPaths()
|
|
{
|
|
const QString path = getConfigurationFilePath(QString());
|
|
sec = QDir::toNativeSeparators(path + ".sec");
|
|
pub = QDir::toNativeSeparators(path + ".pub");
|
|
}
|
|
|
|
QString sec;
|
|
QString pub;
|
|
};
|
|
|
|
QString gpgExecutable()
|
|
{
|
|
return "gpg2";
|
|
}
|
|
|
|
QStringList getDefaultEncryptCommandArguments(const QString &publicKeyPath)
|
|
{
|
|
return QStringList() << "--trust-model" << "always" << "--recipient" << "copyq"
|
|
<< "--charset" << "utf-8" << "--display-charset" << "utf-8" << "--no-tty"
|
|
<< "--no-default-keyring" << "--keyring" << publicKeyPath;
|
|
}
|
|
|
|
void startGpgProcess(QProcess *p, const QStringList &args)
|
|
{
|
|
KeyPairPaths keys;
|
|
p->start(gpgExecutable(), getDefaultEncryptCommandArguments(keys.pub) + args);
|
|
}
|
|
|
|
bool verifyProcess(QProcess *p)
|
|
{
|
|
const int exitCode = p->exitCode();
|
|
if ( p->exitStatus() != QProcess::NormalExit ) {
|
|
log( "ItemEncrypt ERROR: Failed to run GnuPG: " + p->errorString(), LogError );
|
|
return false;
|
|
}
|
|
|
|
if (exitCode != 0) {
|
|
const QString errors = p->readAllStandardError();
|
|
if ( !errors.isEmpty() )
|
|
log( "ItemEncrypt ERROR: GnuPG stderr:\n" + errors, LogError );
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool waitOrTerminate(QProcess *p)
|
|
{
|
|
if ( p->state() != QProcess::NotRunning && !p->waitForFinished() ) {
|
|
p->terminate();
|
|
if ( !p->waitForFinished(5000) )
|
|
p->kill();
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
QString importGpgKey()
|
|
{
|
|
KeyPairPaths keys;
|
|
|
|
QProcess p;
|
|
p.start(gpgExecutable(), getDefaultEncryptCommandArguments(keys.pub) << "--import" << keys.sec);
|
|
if ( !waitOrTerminate(&p) )
|
|
return "Failed to import private key (process timed out).";
|
|
|
|
if ( !verifyProcess(&p) )
|
|
return "Failed to import private key (see log).";
|
|
|
|
return QString();
|
|
}
|
|
|
|
QString exportGpgKey()
|
|
{
|
|
KeyPairPaths keys;
|
|
|
|
// Private key already created or exported.
|
|
if ( QFile::exists(keys.sec) )
|
|
return QString();
|
|
|
|
QProcess p;
|
|
p.start(gpgExecutable(), getDefaultEncryptCommandArguments(keys.pub) << "--export-secret-key" << "copyq");
|
|
if ( !waitOrTerminate(&p) )
|
|
return "Failed to export private key (process timed out).";
|
|
|
|
if ( !verifyProcess(&p) )
|
|
return "Failed to export private key (see log).";
|
|
|
|
QFile secKey(keys.sec);
|
|
if ( !secKey.open(QIODevice::WriteOnly) )
|
|
return "Failed to create private key.";
|
|
|
|
if ( !secKey.setPermissions(QFile::ReadOwner | QFile::WriteOwner) )
|
|
return "Failed to set permissions for private key.";
|
|
|
|
const QByteArray secKeyData = p.readAllStandardOutput();
|
|
secKey.write(secKeyData);
|
|
secKey.close();
|
|
|
|
return QString();
|
|
}
|
|
|
|
QByteArray readGpgOutput(const QStringList &args, const QByteArray &input = QByteArray())
|
|
{
|
|
QProcess p;
|
|
startGpgProcess( &p, args );
|
|
p.write(input);
|
|
p.closeWriteChannel();
|
|
p.waitForFinished();
|
|
verifyProcess(&p);
|
|
return p.readAllStandardOutput();
|
|
}
|
|
|
|
bool keysExist()
|
|
{
|
|
return !readGpgOutput( QStringList("--list-keys") ).isEmpty();
|
|
}
|
|
|
|
bool decryptMimeData(QVariantMap *detinationData, const QModelIndex &index)
|
|
{
|
|
const QVariantMap data = index.data(contentType::data).toMap();
|
|
if ( !data.contains(mimeEncryptedData) )
|
|
return false;
|
|
|
|
const QByteArray encryptedBytes = data.value(mimeEncryptedData).toByteArray();
|
|
const QByteArray bytes = readGpgOutput( QStringList() << "--decrypt", encryptedBytes );
|
|
|
|
return deserializeData(detinationData, bytes);
|
|
}
|
|
|
|
void encryptMimeData(const QVariantMap &data, const QModelIndex &index, QAbstractItemModel *model)
|
|
{
|
|
const QByteArray bytes = serializeData(data);
|
|
const QByteArray encryptedBytes = readGpgOutput( QStringList("--encrypt"), bytes );
|
|
QVariantMap dataMap;
|
|
dataMap.insert(mimeEncryptedData, encryptedBytes);
|
|
model->setData(index, dataMap, contentType::data);
|
|
}
|
|
|
|
void startGenerateKeysProcess(QProcess *process, bool useTransientPasswordlessKey = false)
|
|
{
|
|
const KeyPairPaths keys;
|
|
|
|
auto args = QStringList() << "--batch" << "--gen-key";
|
|
|
|
QByteArray transientOptions;
|
|
if (useTransientPasswordlessKey) {
|
|
args << "--debug-quick-random";
|
|
transientOptions =
|
|
"\n%no-protection"
|
|
"\n%transient-key";
|
|
}
|
|
|
|
startGpgProcess(process, args);
|
|
process->write( "\nKey-Type: RSA"
|
|
"\nKey-Usage: encrypt"
|
|
"\nKey-Length: 2048"
|
|
"\nName-Real: copyq"
|
|
+ transientOptions +
|
|
"\n%secring " + keys.sec.toUtf8() +
|
|
"\n%pubring " + keys.pub.toUtf8() +
|
|
"\n%commit"
|
|
"\n" );
|
|
process->closeWriteChannel();
|
|
}
|
|
|
|
QString exportImportGpgKeys()
|
|
{
|
|
const auto error = exportGpgKey();
|
|
if ( !error.isEmpty() )
|
|
return error;
|
|
|
|
return importGpgKey();
|
|
}
|
|
|
|
bool isGpgInstalled()
|
|
{
|
|
QProcess p;
|
|
startGpgProcess(&p, QStringList("--version"));
|
|
p.closeWriteChannel();
|
|
p.waitForFinished();
|
|
|
|
if (p.exitStatus() != QProcess::NormalExit || p.exitCode() != 0)
|
|
return false;
|
|
|
|
const auto versionOutput = p.readAllStandardOutput();
|
|
return versionOutput.contains(" 2.");
|
|
}
|
|
|
|
} // namespace
|
|
|
|
ItemEncrypted::ItemEncrypted(QWidget *parent)
|
|
: QWidget(parent)
|
|
, ItemWidget(this)
|
|
{
|
|
auto layout = new QVBoxLayout(this);
|
|
layout->setContentsMargins(0, 0, 0, 0);
|
|
|
|
// Show small icon.
|
|
QWidget *iconWidget = new IconWidget(IconLock, this);
|
|
layout->addWidget(iconWidget);
|
|
}
|
|
|
|
void ItemEncrypted::setEditorData(QWidget *editor, const QModelIndex &index) const
|
|
{
|
|
// Decrypt before editing.
|
|
QTextEdit *textEdit = qobject_cast<QTextEdit *>(editor);
|
|
if (textEdit != nullptr) {
|
|
QVariantMap data;
|
|
if ( decryptMimeData(&data, index) ) {
|
|
textEdit->setPlainText( getTextData(data, mimeText) );
|
|
textEdit->selectAll();
|
|
}
|
|
}
|
|
}
|
|
|
|
void ItemEncrypted::setModelData(QWidget *editor, QAbstractItemModel *model,
|
|
const QModelIndex &index) const
|
|
{
|
|
// Encrypt after editing.
|
|
QTextEdit *textEdit = qobject_cast<QTextEdit*>(editor);
|
|
if (textEdit != nullptr)
|
|
encryptMimeData( createDataMap(mimeText, textEdit->toPlainText()), index, model );
|
|
}
|
|
|
|
bool ItemEncryptedSaver::saveItems(const QString &, const QAbstractItemModel &model, QIODevice *file)
|
|
{
|
|
const auto length = model.rowCount();
|
|
if (length == 0)
|
|
return false; // No need to encode empty tab.
|
|
|
|
QByteArray bytes;
|
|
|
|
{
|
|
QDataStream stream(&bytes, QIODevice::WriteOnly);
|
|
stream.setVersion(QDataStream::Qt_4_7);
|
|
|
|
stream << static_cast<quint64>(length);
|
|
|
|
for (int i = 0; i < length && stream.status() == QDataStream::Ok; ++i) {
|
|
QModelIndex index = model.index(i, 0);
|
|
const QVariantMap dataMap = index.data(contentType::data).toMap();
|
|
stream << dataMap;
|
|
}
|
|
}
|
|
|
|
bytes = readGpgOutput(QStringList("--encrypt"), bytes);
|
|
if ( bytes.isEmpty() ) {
|
|
emitEncryptFailed();
|
|
COPYQ_LOG("ItemEncrypt ERROR: Failed to read encrypted data");
|
|
return false;
|
|
}
|
|
|
|
QDataStream stream(file);
|
|
stream << QString(dataFileHeaderV2);
|
|
stream.writeRawData( bytes.data(), bytes.size() );
|
|
|
|
if ( stream.status() != QDataStream::Ok ) {
|
|
emitEncryptFailed();
|
|
COPYQ_LOG("ItemEncrypt ERROR: Failed to write encrypted data");
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void ItemEncryptedSaver::emitEncryptFailed()
|
|
{
|
|
emit error( ItemEncryptedLoader::tr("Encryption failed!") );
|
|
}
|
|
|
|
bool ItemEncryptedScriptable::isEncrypted()
|
|
{
|
|
const auto args = currentArguments();
|
|
for (const auto &arg : args) {
|
|
bool ok;
|
|
const int row = arg.toInt(&ok);
|
|
if (ok) {
|
|
const auto result = call("read", QVariantList() << "?" << row);
|
|
if ( result.toByteArray().contains(mimeEncryptedData) )
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
QByteArray ItemEncryptedScriptable::encrypt()
|
|
{
|
|
const auto args = currentArguments();
|
|
const auto bytes = args.first().toByteArray();
|
|
return encrypt(bytes);
|
|
}
|
|
|
|
QByteArray ItemEncryptedScriptable::decrypt()
|
|
{
|
|
const auto args = currentArguments();
|
|
const auto bytes = args.first().toByteArray();
|
|
return decrypt(bytes);
|
|
}
|
|
|
|
void ItemEncryptedScriptable::encryptItem()
|
|
{
|
|
QVariantMap dataMap;
|
|
const auto formats = call("dataFormats").toList();
|
|
for (const auto &formatValue : formats) {
|
|
const auto format = formatValue.toString();
|
|
if ( !format.startsWith(COPYQ_MIME_PREFIX) ) {
|
|
const auto data = call("data", QVariantList() << format).toByteArray();
|
|
dataMap.insert(format, data);
|
|
}
|
|
}
|
|
|
|
const auto bytes = call("pack", QVariantList() << dataMap).toByteArray();
|
|
const auto encryptedBytes = encrypt(bytes);
|
|
if (encryptedBytes.isEmpty())
|
|
return;
|
|
|
|
call("setData", QVariantList() << mimeEncryptedData << encryptedBytes);
|
|
|
|
for ( const auto &format : dataMap.keys() )
|
|
call("removeData", QVariantList() << format);
|
|
}
|
|
|
|
void ItemEncryptedScriptable::decryptItem()
|
|
{
|
|
const auto encryptedBytes = call("data", QVariantList() << mimeEncryptedData).toByteArray();
|
|
const auto itemData = decrypt(encryptedBytes);
|
|
if (itemData.isEmpty())
|
|
return;
|
|
|
|
const auto dataMap = call("unpack", QVariantList() << itemData).toMap();
|
|
for ( const auto &format : dataMap.keys() )
|
|
call("setData", QVariantList() << format << dataMap[format]);
|
|
}
|
|
|
|
void ItemEncryptedScriptable::encryptItems()
|
|
{
|
|
const auto dataValueList = call("selectedItemsData").toList();
|
|
|
|
QVariantList dataList;
|
|
for (const auto &itemDataValue : dataValueList) {
|
|
auto itemData = itemDataValue.toMap();
|
|
|
|
QVariantMap itemDataToEncrypt;
|
|
for ( const auto &format : itemData.keys() ) {
|
|
if ( !format.startsWith(COPYQ_MIME_PREFIX) ) {
|
|
itemDataToEncrypt.insert(format, itemData[format]);
|
|
itemData.remove(format);
|
|
}
|
|
}
|
|
|
|
const auto bytes = call("pack", QVariantList() << itemDataToEncrypt).toByteArray();
|
|
const auto encryptedBytes = encrypt(bytes);
|
|
if (encryptedBytes.isEmpty())
|
|
return;
|
|
itemData.insert(mimeEncryptedData, encryptedBytes);
|
|
|
|
dataList.append(itemData);
|
|
}
|
|
|
|
call( "setSelectedItemsData", QVariantList() << QVariant(dataList) );
|
|
}
|
|
|
|
void ItemEncryptedScriptable::decryptItems()
|
|
{
|
|
const auto dataValueList = call("selectedItemsData").toList();
|
|
|
|
QVariantList dataList;
|
|
for (const auto &itemDataValue : dataValueList) {
|
|
auto itemData = itemDataValue.toMap();
|
|
|
|
const auto encryptedBytes = itemData.value(mimeEncryptedData).toByteArray();
|
|
if ( !encryptedBytes.isEmpty() ) {
|
|
itemData.remove(mimeEncryptedData);
|
|
|
|
const auto decryptedBytes = decrypt(encryptedBytes);
|
|
if (decryptedBytes.isEmpty())
|
|
return;
|
|
|
|
const auto decryptedItemData = call("unpack", QVariantList() << decryptedBytes).toMap();
|
|
for ( const auto &format : decryptedItemData.keys() )
|
|
itemData.insert(format, decryptedItemData[format]);
|
|
}
|
|
|
|
dataList.append(itemData);
|
|
}
|
|
|
|
call( "setSelectedItemsData", QVariantList() << QVariant(dataList) );
|
|
}
|
|
|
|
void ItemEncryptedScriptable::copyEncryptedItems()
|
|
{
|
|
const auto dataValueList = call("selectedItemsData").toList();
|
|
QString text;
|
|
for (const auto &dataValue : dataValueList) {
|
|
if ( !text.isEmpty() )
|
|
text.append('\n');
|
|
|
|
const auto data = dataValue.toMap();
|
|
const auto itemTextValue = data.value(mimeText);
|
|
if ( itemTextValue.isValid() ) {
|
|
text.append( getTextData(itemTextValue.toByteArray()) );
|
|
} else {
|
|
const auto encryptedBytes = data.value(mimeEncryptedData).toByteArray();
|
|
if ( !encryptedBytes.isEmpty() ) {
|
|
const auto itemData = decrypt(encryptedBytes);
|
|
if (itemData.isEmpty())
|
|
return;
|
|
const auto dataMap = call("unpack", QVariantList() << itemData).toMap();
|
|
text.append( getTextData(dataMap) );
|
|
}
|
|
}
|
|
}
|
|
|
|
call("copy", QVariantList() << text);
|
|
}
|
|
|
|
QString ItemEncryptedScriptable::generateTestKeys()
|
|
{
|
|
const KeyPairPaths keys;
|
|
for ( const auto &keyFileName : {keys.sec, keys.pub} ) {
|
|
if ( QFile::exists(keyFileName) && !QFile::remove(keyFileName) )
|
|
return QString("Failed to remove \"%1\"").arg(keys.sec);
|
|
}
|
|
|
|
QProcess process;
|
|
startGenerateKeysProcess(&process, true);
|
|
|
|
if ( !waitOrTerminate(&process) || !verifyProcess(&process) ) {
|
|
return QString("ItemEncrypt ERROR: %1; stderr: %2")
|
|
.arg( process.errorString() )
|
|
.arg( QString::fromUtf8(process.readAllStandardError()) );
|
|
}
|
|
|
|
const auto error = exportImportGpgKeys();
|
|
if ( !error.isEmpty() )
|
|
return error;
|
|
|
|
for ( const auto &keyFileName : {keys.sec, keys.pub} ) {
|
|
if ( !QFile::exists(keyFileName) )
|
|
return QString("Failed to create \"%1\"").arg(keys.sec);
|
|
}
|
|
|
|
return QString();
|
|
}
|
|
|
|
bool ItemEncryptedScriptable::isGpgInstalled()
|
|
{
|
|
return ::isGpgInstalled();
|
|
}
|
|
|
|
QByteArray ItemEncryptedScriptable::encrypt(const QByteArray &bytes)
|
|
{
|
|
const auto encryptedBytes = readGpgOutput(QStringList("--encrypt"), bytes);
|
|
if ( encryptedBytes.isEmpty() )
|
|
eval("throw 'Failed to execute GPG!'");
|
|
return encryptedBytes;
|
|
}
|
|
|
|
QByteArray ItemEncryptedScriptable::decrypt(const QByteArray &bytes)
|
|
{
|
|
const auto decryptedBytes = readGpgOutput(QStringList("--decrypt"), bytes);
|
|
if ( decryptedBytes.isEmpty() )
|
|
eval("throw 'Failed to execute GPG!'");
|
|
return decryptedBytes;
|
|
}
|
|
|
|
ItemEncryptedLoader::ItemEncryptedLoader()
|
|
: ui()
|
|
, m_settings()
|
|
, m_gpgProcessStatus(GpgNotRunning)
|
|
, m_gpgProcess(nullptr)
|
|
{
|
|
}
|
|
|
|
ItemEncryptedLoader::~ItemEncryptedLoader()
|
|
{
|
|
terminateGpgProcess();
|
|
}
|
|
|
|
ItemWidget *ItemEncryptedLoader::create(const QModelIndex &index, QWidget *parent, bool) const
|
|
{
|
|
if ( index.data(contentType::isHidden).toBool() )
|
|
return nullptr;
|
|
|
|
const QVariantMap dataMap = index.data(contentType::data).toMap();
|
|
return dataMap.contains(mimeEncryptedData) ? new ItemEncrypted(parent) : nullptr;
|
|
}
|
|
|
|
QStringList ItemEncryptedLoader::formatsToSave() const
|
|
{
|
|
return QStringList(mimeEncryptedData);
|
|
}
|
|
|
|
QVariantMap ItemEncryptedLoader::applySettings()
|
|
{
|
|
Q_ASSERT(ui != nullptr);
|
|
m_settings.insert( "encrypt_tabs", ui->plainTextEditEncryptTabs->toPlainText().split('\n') );
|
|
return m_settings;
|
|
}
|
|
|
|
QWidget *ItemEncryptedLoader::createSettingsWidget(QWidget *parent)
|
|
{
|
|
ui.reset(new Ui::ItemEncryptedSettings);
|
|
QWidget *w = new QWidget(parent);
|
|
ui->setupUi(w);
|
|
|
|
connect( ui->pushButtonAddCommands, SIGNAL(clicked()),
|
|
this, SLOT(addCommands()) );
|
|
|
|
ui->plainTextEditEncryptTabs->setPlainText(
|
|
m_settings.value("encrypt_tabs").toStringList().join("\n") );
|
|
|
|
// Check if gpg application is available.
|
|
if ( !isGpgInstalled() ) {
|
|
m_gpgProcessStatus = GpgNotInstalled;
|
|
} else {
|
|
KeyPairPaths keys;
|
|
ui->labelShareInfo->setTextFormat(Qt::RichText);
|
|
ui->labelShareInfo->setText( tr("To share encrypted items on other computer or"
|
|
" session, you'll need public and secret key files:"
|
|
"<ul>"
|
|
"<li>%1</li>"
|
|
"<li>%2<br />(Keep this secret key in a safe place.)</li>"
|
|
"</ul>"
|
|
)
|
|
.arg( quoteString(keys.pub) )
|
|
.arg( quoteString(keys.sec) )
|
|
);
|
|
}
|
|
|
|
updateUi();
|
|
|
|
connect( ui->pushButtonPassword, SIGNAL(clicked()),
|
|
this, SLOT(setPassword()) );
|
|
|
|
return w;
|
|
}
|
|
|
|
bool ItemEncryptedLoader::canLoadItems(QIODevice *file) const
|
|
{
|
|
QDataStream stream(file);
|
|
|
|
QString header;
|
|
stream >> header;
|
|
|
|
return stream.status() == QDataStream::Ok
|
|
&& (header == dataFileHeader || header == dataFileHeaderV2);
|
|
}
|
|
|
|
bool ItemEncryptedLoader::canSaveItems(const QString &tabName) const
|
|
{
|
|
for ( const auto &encryptTabName : m_settings.value("encrypt_tabs").toStringList() ) {
|
|
if ( encryptTabName.isEmpty() )
|
|
continue;
|
|
|
|
QString tabName1 = tabName;
|
|
|
|
// Ignore ampersands (usually just for underlining mnemonics) if none is specified.
|
|
if ( !hasKeyHint(encryptTabName) )
|
|
removeKeyHint(&tabName1);
|
|
|
|
// Ignore path in tab tree if none path separator is specified.
|
|
if ( !encryptTabName.contains('/') ) {
|
|
const int i = tabName1.lastIndexOf('/');
|
|
tabName1.remove(0, i + 1);
|
|
}
|
|
|
|
if ( tabName1 == encryptTabName )
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
ItemSaverPtr ItemEncryptedLoader::loadItems(const QString &, QAbstractItemModel *model, QIODevice *file, int maxItems)
|
|
{
|
|
// This is needed to skip header.
|
|
if ( !canLoadItems(file) )
|
|
return nullptr;
|
|
|
|
if (m_gpgProcessStatus == GpgNotInstalled) {
|
|
emit error( tr("GnuPG must be installed to view encrypted tabs.") );
|
|
return nullptr;
|
|
}
|
|
|
|
importGpgKey();
|
|
|
|
QProcess p;
|
|
startGpgProcess( &p, QStringList("--decrypt") );
|
|
|
|
char encryptedBytes[4096];
|
|
|
|
QDataStream stream(file);
|
|
while ( !stream.atEnd() ) {
|
|
const int bytesRead = stream.readRawData(encryptedBytes, 4096);
|
|
if (bytesRead == -1) {
|
|
emitDecryptFailed();
|
|
COPYQ_LOG("ItemEncrypted ERROR: Failed to read encrypted data");
|
|
return nullptr;
|
|
}
|
|
p.write(encryptedBytes, bytesRead);
|
|
}
|
|
|
|
p.closeWriteChannel();
|
|
if ( !waitOrTerminate(&p) || !verifyProcess(&p) ) {
|
|
emitDecryptFailed();
|
|
return nullptr;
|
|
}
|
|
|
|
const QByteArray bytes = p.readAllStandardOutput();
|
|
if ( bytes.isEmpty() ) {
|
|
emitDecryptFailed();
|
|
COPYQ_LOG("ItemEncrypt ERROR: Failed to read encrypted data.");
|
|
verifyProcess(&p);
|
|
return nullptr;
|
|
}
|
|
|
|
QDataStream stream2(bytes);
|
|
|
|
quint64 length;
|
|
stream2 >> length;
|
|
if ( length <= 0 || stream2.status() != QDataStream::Ok ) {
|
|
emitDecryptFailed();
|
|
COPYQ_LOG("ItemEncrypt ERROR: Failed to parse item count!");
|
|
return nullptr;
|
|
}
|
|
length = qMin(length, static_cast<quint64>(maxItems)) - static_cast<quint64>(model->rowCount());
|
|
|
|
const auto count = length < maxItemCount ? static_cast<int>(length) : maxItemCount;
|
|
for ( int i = 0; i < count && stream2.status() == QDataStream::Ok; ++i ) {
|
|
if ( !model->insertRow(i) ) {
|
|
emitDecryptFailed();
|
|
COPYQ_LOG("ItemEncrypt ERROR: Failed to insert item!");
|
|
return nullptr;
|
|
}
|
|
QVariantMap dataMap;
|
|
stream2 >> dataMap;
|
|
model->setData( model->index(i, 0), dataMap, contentType::data );
|
|
}
|
|
|
|
if ( stream2.status() != QDataStream::Ok ) {
|
|
emitDecryptFailed();
|
|
COPYQ_LOG("ItemEncrypt ERROR: Failed to decrypt item!");
|
|
return nullptr;
|
|
}
|
|
|
|
return createSaver();
|
|
}
|
|
|
|
ItemSaverPtr ItemEncryptedLoader::initializeTab(const QString &, QAbstractItemModel *, int)
|
|
{
|
|
if (m_gpgProcessStatus == GpgNotInstalled)
|
|
return nullptr;
|
|
|
|
return createSaver();
|
|
}
|
|
|
|
QObject *ItemEncryptedLoader::tests(const TestInterfacePtr &test) const
|
|
{
|
|
#ifdef HAS_TESTS
|
|
QObject *tests = new ItemEncryptedTests(test);
|
|
return tests;
|
|
#else
|
|
Q_UNUSED(test);
|
|
return nullptr;
|
|
#endif
|
|
}
|
|
|
|
ItemScriptable *ItemEncryptedLoader::scriptableObject(QObject *parent)
|
|
{
|
|
return new ItemEncryptedScriptable(parent);
|
|
}
|
|
|
|
QList<Command> ItemEncryptedLoader::commands() const
|
|
{
|
|
QList<Command> commands;
|
|
|
|
Command c;
|
|
c.name = tr("Encrypt (needs GnuPG)");
|
|
c.icon = QString(QChar(IconLock));
|
|
c.input = "!OUTPUT";
|
|
c.output = mimeEncryptedData;
|
|
c.inMenu = true;
|
|
c.cmd = "copyq: plugins.itemencrypted.encryptItems()";
|
|
c.shortcuts.append( toPortableShortcutText(tr("Ctrl+L")) );
|
|
commands.append(c);
|
|
|
|
c = Command();
|
|
c.name = tr("Decrypt");
|
|
c.icon = QString(QChar(IconUnlock));
|
|
c.input = mimeEncryptedData;
|
|
c.output = mimeItems;
|
|
c.inMenu = true;
|
|
c.cmd = "copyq: plugins.itemencrypted.decryptItems()";
|
|
c.shortcuts.append( toPortableShortcutText(tr("Ctrl+L")) );
|
|
commands.append(c);
|
|
|
|
c = Command();
|
|
c.name = tr("Decrypt and Copy");
|
|
c.icon = QString(QChar(IconUnlockAlt));
|
|
c.input = mimeEncryptedData;
|
|
c.inMenu = true;
|
|
c.cmd = "copyq: plugins.itemencrypted.copyEncryptedItems()";
|
|
c.shortcuts.append( toPortableShortcutText(tr("Ctrl+Shift+L")) );
|
|
commands.append(c);
|
|
|
|
return commands;
|
|
}
|
|
|
|
void ItemEncryptedLoader::setPassword()
|
|
{
|
|
if (m_gpgProcessStatus == GpgGeneratingKeys)
|
|
return;
|
|
|
|
if (m_gpgProcess != nullptr) {
|
|
terminateGpgProcess();
|
|
return;
|
|
}
|
|
|
|
if ( !keysExist() ) {
|
|
m_gpgProcessStatus = GpgGeneratingKeys;
|
|
m_gpgProcess = new QProcess(this);
|
|
startGenerateKeysProcess(m_gpgProcess);
|
|
} else {
|
|
// Change password.
|
|
m_gpgProcessStatus = GpgChangingPassword;
|
|
m_gpgProcess = new QProcess(this);
|
|
startGpgProcess( m_gpgProcess, QStringList() << "--edit-key" << "copyq" << "passwd" << "save");
|
|
}
|
|
|
|
m_gpgProcess->waitForStarted();
|
|
if ( m_gpgProcess->state() == QProcess::NotRunning ) {
|
|
onGpgProcessFinished( m_gpgProcess->exitCode(), m_gpgProcess->exitStatus() );
|
|
} else {
|
|
connect( m_gpgProcess, SIGNAL(finished(int,QProcess::ExitStatus)),
|
|
this, SLOT(onGpgProcessFinished(int,QProcess::ExitStatus)) );
|
|
updateUi();
|
|
}
|
|
}
|
|
|
|
void ItemEncryptedLoader::terminateGpgProcess()
|
|
{
|
|
if (m_gpgProcess == nullptr)
|
|
return;
|
|
QProcess *p = m_gpgProcess;
|
|
m_gpgProcess = nullptr;
|
|
p->terminate();
|
|
p->waitForFinished();
|
|
p->deleteLater();
|
|
m_gpgProcessStatus = GpgNotRunning;
|
|
updateUi();
|
|
}
|
|
|
|
void ItemEncryptedLoader::onGpgProcessFinished(int exitCode, QProcess::ExitStatus exitStatus)
|
|
{
|
|
QString error;
|
|
|
|
if (m_gpgProcess != nullptr) {
|
|
if (ui != nullptr) {
|
|
if (exitStatus != QProcess::NormalExit)
|
|
error = m_gpgProcess->errorString();
|
|
else if (exitCode != 0)
|
|
error = getTextData(m_gpgProcess->readAllStandardError());
|
|
else if ( m_gpgProcess->error() != QProcess::UnknownError )
|
|
error = m_gpgProcess->errorString();
|
|
else if ( !keysExist() )
|
|
error = tr("Failed to generate keys.");
|
|
}
|
|
|
|
m_gpgProcess->deleteLater();
|
|
m_gpgProcess = nullptr;
|
|
}
|
|
|
|
// Export and import private key to a file in configuration.
|
|
if ( m_gpgProcessStatus == GpgGeneratingKeys && error.isEmpty() )
|
|
error = exportImportGpgKeys();
|
|
|
|
if (!error.isEmpty())
|
|
error = tr("Error: %1").arg(error);
|
|
|
|
m_gpgProcessStatus = GpgNotRunning;
|
|
|
|
updateUi();
|
|
ui->labelInfo->setText( error.isEmpty() ? tr("Done") : error );
|
|
}
|
|
|
|
void ItemEncryptedLoader::addCommands()
|
|
{
|
|
emit addCommands(commands());
|
|
}
|
|
|
|
void ItemEncryptedLoader::updateUi()
|
|
{
|
|
if (ui == nullptr)
|
|
return;
|
|
|
|
if (m_gpgProcessStatus == GpgNotInstalled) {
|
|
ui->labelInfo->setText("To use item encryption, install"
|
|
" <a href=\"http://www.gnupg.org/\">GnuPG</a>"
|
|
" application and restart CopyQ.");
|
|
ui->pushButtonPassword->hide();
|
|
ui->pushButtonAddCommands->hide();
|
|
ui->groupBoxEncryptTabs->hide();
|
|
ui->groupBoxShareInfo->hide();
|
|
} else if (m_gpgProcessStatus == GpgGeneratingKeys) {
|
|
ui->labelInfo->setText( tr("Creating new keys (this may take a few minutes)...") );
|
|
ui->pushButtonPassword->setText( tr("Cancel") );
|
|
} else if (m_gpgProcessStatus == GpgChangingPassword) {
|
|
ui->labelInfo->setText( tr("Setting new password...") );
|
|
ui->pushButtonPassword->setText( tr("Cancel") );
|
|
} else if ( !keysExist() ) {
|
|
ui->labelInfo->setText( tr("Encryption keys <strong>must be generated</strong>"
|
|
" before item encryption can be used.") );
|
|
ui->pushButtonPassword->setText( tr("Generate New Keys...") );
|
|
} else {
|
|
ui->pushButtonPassword->setText( tr("Change Password...") );
|
|
}
|
|
}
|
|
|
|
void ItemEncryptedLoader::emitDecryptFailed()
|
|
{
|
|
emit error( tr("Decryption failed!") );
|
|
}
|
|
|
|
ItemSaverPtr ItemEncryptedLoader::createSaver()
|
|
{
|
|
auto saver = std::make_shared<ItemEncryptedSaver>();
|
|
connect( saver.get(), SIGNAL(error(QString)),
|
|
this, SIGNAL(error(QString)) );
|
|
return saver;
|
|
}
|
|
|
|
Q_EXPORT_PLUGIN2(itemencrypted, ItemEncryptedLoader)
|