diff --git a/LeftPanel.qml b/LeftPanel.qml index 5fc3ff33..fb4c7397 100644 --- a/LeftPanel.qml +++ b/LeftPanel.qml @@ -47,6 +47,7 @@ Rectangle { signal settingsClicked() signal addressBookClicked() signal miningClicked() + signal signClicked() function selectItem(pos) { menuColumn.previousButton.checked = false @@ -57,6 +58,7 @@ Rectangle { else if(pos === "AddressBook") menuColumn.previousButton = addressBookButton else if(pos === "Mining") menuColumn.previousButton = miningButton else if(pos === "TxKey") menuColumn.previousButton = txkeyButton + else if(pos === "Sign") menuColumn.previousButton = signButton else if(pos === "Settings") menuColumn.previousButton = settingsButton menuColumn.previousButton.checked = true @@ -352,6 +354,20 @@ Rectangle { height: 1 } */ + // ------------- Sign/verify tab --------------- + MenuButton { + id: signButton + anchors.left: parent.left + anchors.right: parent.right + text: qsTr("Sign/verify") + translationManager.emptyString + symbol: qsTr("S") + translationManager.emptyString + dotColor: "#AAFFBB" + onClicked: { + parent.previousButton.checked = false + parent.previousButton = signButton + panel.signClicked() + } + } // ------------- Settings tab --------------- MenuButton { id: settingsButton diff --git a/MiddlePanel.qml b/MiddlePanel.qml index f1254760..692be813 100644 --- a/MiddlePanel.qml +++ b/MiddlePanel.qml @@ -47,6 +47,7 @@ Rectangle { property Receive receiveView: Receive { } property TxKey txkeyView: TxKey { } property History historyView: History { } + property Sign signView: Sign { } property Settings settingsView: Settings { } @@ -117,6 +118,9 @@ Rectangle { }, State { name: "AddressBook" PropertyChanges { /*TODO*/ } + }, State { + name: "Sign" + PropertyChanges { target: root; currentView: signView } }, State { name: "Settings" PropertyChanges { target: root; currentView: settingsView } diff --git a/main.qml b/main.qml index 6f5bdde4..9872409d 100644 --- a/main.qml +++ b/main.qml @@ -724,6 +724,7 @@ ApplicationWindow { onTxkeyClicked: middlePanel.state = "TxKey" onAddressBookClicked: middlePanel.state = "AddressBook" onMiningClicked: middlePanel.state = "Minning" + onSignClicked: middlePanel.state = "Sign" onSettingsClicked: middlePanel.state = "Settings" } diff --git a/pages/Sign.qml b/pages/Sign.qml new file mode 100644 index 00000000..58e07278 --- /dev/null +++ b/pages/Sign.qml @@ -0,0 +1,499 @@ +// Copyright (c) 2014-2015, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import QtQuick 2.0 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 +import QtQuick.Layouts 1.1 +import QtQuick.Dialogs 1.2 + +import "../components" +import moneroComponents.Clipboard 1.0 +import moneroComponents.WalletManager 1.0 + +Rectangle { + id: mainLayout + + property int labelWidth: 120 + property int editWidth: 400 + property int lineEditFontSize: 12 + + color: "#F0EEEE" + + Clipboard { id: clipboard } + + function checkAddress(address, testnet) { + return walletManager.addressValid(address, testnet) + } + + MessageDialog { + // dynamically change onclose handler + property var onCloseCallback + id: signatureVerificationMessage + standardButtons: StandardButton.Ok + onAccepted: { + if (onCloseCallback) { + onCloseCallback() + } + } + } + + function displayVerificationResult(result) { + if (result) { + signatureVerificationMessage.title = qsTr("Good signature") + translationManager.emptyString + signatureVerificationMessage.text = qsTr("This is a good signature") + translationManager.emptyString + signatureVerificationMessage.icon = StandardIcon.Information + } + else { + signatureVerificationMessage.title = qsTr("Bad signature") + translationManager.emptyString + signatureVerificationMessage.text = qsTr("This signature did not verify") + translationManager.emptyString + signatureVerificationMessage.icon = StandardIcon.Critical + } + signatureVerificationMessage.open() + } + + // ================ + // Sign a message + // message: [ ] [SIGN] + // [SELECT] file: [ ] [SIGN] + // signature: [ ] + // ================ + // verify a message + // address: [ ] + // message: [ ] [VERIFY] + // [SELECT] file: [ ] [VERIFY] + // signature: [ ] + // ================ + + // sign / verify + ColumnLayout { + anchors.margins: 10 + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + + spacing: 20 + + Rectangle { + anchors.fill: signBox + color: "#00000000" + border.width: 2 + border.color: "#CCCCCC" + anchors.margins: -15 + } + + // sign + ColumnLayout { + id: signBox + anchors.margins: 40 + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + + RowLayout { + ColumnLayout { + spacing: 8 + Label { + text: qsTr("Sign a message or file contents with your address:") + translationManager.emptyString + fontSize: 18 + } + Label {} + } + } + + Label { + id: signMessageLabel + fontSize: 14 + text: qsTr("Either message:") + translationManager.emptyString + width: mainLayout.labelWidth + } + + RowLayout { + id: signMessageRow + anchors.topMargin: 17 + anchors.left: parent.left + anchors.right: parent.right + + LineEdit { + id: signMessageLine + anchors.left: parent.left + anchors.right: signMessageButton.left + fontSize: mainLayout.lineEditFontSize + placeholderText: qsTr("Message to sign") + translationManager.emptyString; + readOnly: false + Layout.fillWidth: true + onTextChanged: signSignatureLine.text = "" + + IconButton { + imageSource: "../images/copyToClipboard.png" + onClicked: { + if (signMessageLine.text.length > 0) { + clipboard.setText(signMessageLine.text) + } + } + } + } + + StandardButton { + id: signMessageButton + anchors.right: parent.right + width: 60 + text: qsTr("SIGN") + translationManager.emptyString + shadowReleasedColor: "#FF4304" + shadowPressedColor: "#B32D00" + releasedColor: "#FF6C3C" + pressedColor: "#FF4304" + enabled: true + onClicked: { + var signature = appWindow.currentWallet.signMessage(signMessageLine.text, false) + signSignatureLine.text = signature + } + } + } + + Label { + id: signMessageFileLabel + fontSize: 14 + text: qsTr("Or file:") + translationManager.emptyString + width: mainLayout.labelWidth + } + + RowLayout { + id: signFileRow + anchors.topMargin: 17 + anchors.left: parent.left + anchors.right: parent.right + + FileDialog { + id: signFileDialog + title: "Please choose a file to sign" + folder: "file://" + nameFilters: [ "*"] + + onAccepted: { + signFileLine.text = walletManager.urlToLocalPath(signFileDialog.fileUrl) + } + } + + StandardButton { + id: loadFileToSignButton + anchors.rightMargin: 17 + width: 60 + text: qsTr("SELECT") + translationManager.emptyString + shadowReleasedColor: "#FF4304" + shadowPressedColor: "#B32D00" + releasedColor: "#FF6C3C" + pressedColor: "#FF4304" + enabled: true + onClicked: { + signFileDialog.open() + } + } + LineEdit { + id: signFileLine + anchors.left: loadFileToSignButton.right + anchors.right: signFileButton.left + fontSize: mainLayout.lineEditFontSize + placeholderText: qsTr("Filename with message to sign") + translationManager.emptyString; + readOnly: false + Layout.fillWidth: true + onTextChanged: signSignatureLine.text = "" + + IconButton { + imageSource: "../images/copyToClipboard.png" + onClicked: { + if (signFileLine.text.length > 0) { + clipboard.setText(signFileLine.text) + } + } + } + } + + StandardButton { + id: signFileButton + anchors.right: parent.right + width: 60 + text: qsTr("SIGN") + translationManager.emptyString + shadowReleasedColor: "#FF4304" + shadowPressedColor: "#B32D00" + releasedColor: "#FF6C3C" + pressedColor: "#FF4304" + enabled: true + onClicked: { + var signature = appWindow.currentWallet.signMessage(signFileLine.text, true) + signSignatureLine.text = signature + } + } + } + + RowLayout { + id: signSignatureRow + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 17 + + Label { + id: signSignatureLabel + fontSize: 14 + text: qsTr("Signature") + translationManager.emptyString + width: mainLayout.labelWidth + } + + LineEdit { + id: signSignatureLine + anchors.left: signSignatureLabel.right + anchors.right: parent.right + fontSize: mainLayout.lineEditFontSize + placeholderText: qsTr("Signature") + translationManager.emptyString; + readOnly: true + Layout.fillWidth: true + + IconButton { + imageSource: "../images/copyToClipboard.png" + onClicked: { + if (signSignatureLine.text.length > 0) { + clipboard.setText(signSignatureLine.text) + } + } + } + } + } + } + + Rectangle { + anchors.fill: verifyBox + color: "#00000000" + border.width: 2 + border.color: "#CCCCCC" + anchors.margins: -15 + } + + // verify + ColumnLayout { + id: verifyBox + anchors.margins: 40 + anchors.left: parent.left + anchors.right: parent.right + anchors.top: signBox.bottom + + RowLayout { + ColumnLayout { + spacing: 8 + Label { + text: qsTr("Verify a message or file signature from an address:") + translationManager.emptyString + fontSize: 18 + } + Label {} + } + } + + Label { + id: verifyMessageLabel + fontSize: 14 + text: qsTr("Either message:") + translationManager.emptyString + width: mainLayout.labelWidth + } + + RowLayout { + id: verifyMessageRow + anchors.topMargin: 17 + anchors.left: parent.left + anchors.right: parent.right + + LineEdit { + id: verifyMessageLine + anchors.left: parent.left + anchors.right: verifyMessageButton.left + fontSize: mainLayout.lineEditFontSize + placeholderText: qsTr("Message to verify") + translationManager.emptyString; + readOnly: false + Layout.fillWidth: true + + IconButton { + imageSource: "../images/copyToClipboard.png" + onClicked: { + if (verifyMessageLine.text.length > 0) { + clipboard.setText(verifyMessageLine.text) + } + } + } + } + + StandardButton { + id: verifyMessageButton + anchors.right: parent.right + width: 60 + text: qsTr("VERIFY") + translationManager.emptyString + shadowReleasedColor: "#FF4304" + shadowPressedColor: "#B32D00" + releasedColor: "#FF6C3C" + pressedColor: "#FF4304" + enabled: true + onClicked: { + var verified = appWindow.currentWallet.verifySignedMessage(verifyMessageLine.text, verifyAddressLine.text, verifySignatureLine.text, false) + displayVerificationResult(verified) + } + } + } + + Label { + id: verifyMessageFileLabel + fontSize: 14 + text: qsTr("Or file:") + translationManager.emptyString + width: mainLayout.labelWidth + } + + RowLayout { + id: verifyFileRow + anchors.topMargin: 17 + anchors.left: parent.left + anchors.right: parent.right + + FileDialog { + id: verifyFileDialog + title: "Please choose a file to verify" + folder: "file://" + nameFilters: [ "*"] + + onAccepted: { + verifyFileLine.text = walletManager.urlToLocalPath(verifyFileDialog.fileUrl) + } + } + + StandardButton { + id: loadFileToVerifyButton + anchors.rightMargin: 17 + width: 60 + text: qsTr("SELECT") + translationManager.emptyString + shadowReleasedColor: "#FF4304" + shadowPressedColor: "#B32D00" + releasedColor: "#FF6C3C" + pressedColor: "#FF4304" + enabled: true + onClicked: { + verifyFileDialog.open() + } + } + LineEdit { + id: verifyFileLine + anchors.left: loadFileToVerifyButton.right + anchors.right: verifyFileButton.left + fontSize: mainLayout.lineEditFontSize + placeholderText: qsTr("Filename with message to verify") + translationManager.emptyString; + readOnly: false + Layout.fillWidth: true + + IconButton { + imageSource: "../images/copyToClipboard.png" + onClicked: { + if (verifyFileLine.text.length > 0) { + clipboard.setText(verifyFileLine.text) + } + } + } + } + + StandardButton { + id: verifyFileButton + anchors.right: parent.right + width: 60 + text: qsTr("VERIFY") + translationManager.emptyString + shadowReleasedColor: "#FF4304" + shadowPressedColor: "#B32D00" + releasedColor: "#FF6C3C" + pressedColor: "#FF4304" + enabled: true + onClicked: { + var verified = appWindow.currentWallet.verifySignedMessage(verifyFileLine.text, verifyAddressLine.text, verifySignatureLine.text, true) + displayVerificationResult(verified) + } + } + } + + Label { + id: verifyAddressLabel + fontSize: 14 + width: mainLayout.labelWidth + textFormat: Text.RichText + text: qsTr("\ + Signing address ( Type in or select from Address book )") + + translationManager.emptyString + + onLinkActivated: appWindow.showPageRequest("AddressBook") + } + + LineEdit { + id: verifyAddressLine + anchors.left: parent.left + anchors.right: parent.right + anchors.top: verifyAddressLabel.bottom + anchors.topMargin: 5 + placeholderText: "4..." + // validator: RegExpValidator { regExp: /[0-9A-Fa-f]{95}/g } + } + + RowLayout { + id: verifySignatureRow + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 17 + + Label { + id: verifySignatureLabel + fontSize: 14 + text: qsTr("Signature") + translationManager.emptyString + width: mainLayout.labelWidth + } + + LineEdit { + id: verifySignatureLine + anchors.left: verifySignatureLabel.right + anchors.right: parent.right + fontSize: mainLayout.lineEditFontSize + placeholderText: qsTr("Signature") + translationManager.emptyString; + Layout.fillWidth: true + + IconButton { + imageSource: "../images/copyToClipboard.png" + onClicked: { + if (verifySignatureLine.text.length > 0) { + clipboard.setText(verifySignatureLine.text) + } + } + } + } + } + } + } + + function onPageCompleted() { + console.log("Sign/verify page loaded"); + } + +} diff --git a/qml.qrc b/qml.qrc index f54d2d0f..17873589 100644 --- a/qml.qrc +++ b/qml.qrc @@ -119,5 +119,6 @@ components/ProcessingSplash.qml components/DaemonProgress.qml components/StandardDialog.qml + pages/Sign.qml diff --git a/src/libwalletqt/Wallet.cpp b/src/libwalletqt/Wallet.cpp index d281c03a..18ded2c8 100644 --- a/src/libwalletqt/Wallet.cpp +++ b/src/libwalletqt/Wallet.cpp @@ -275,6 +275,78 @@ QString Wallet::getTxKey(const QString &txid) const return QString::fromStdString(m_walletImpl->getTxKey(txid.toStdString())); } +QString Wallet::signMessage(const QString &message, bool filename) const +{ + if (filename) { + QFile file(message); + uchar *data = NULL; + + try { + if (!file.open(QIODevice::ReadOnly)) + return ""; + quint64 size = file.size(); + if (size == 0) { + file.close(); + return QString::fromStdString(m_walletImpl->signMessage(std::string())); + } + data = file.map(0, size); + if (!data) { + file.close(); + return ""; + } + std::string signature = m_walletImpl->signMessage(std::string((const char*)data, size)); + file.unmap(data); + file.close(); + return QString::fromStdString(signature); + } + catch (const std::exception &e) { + if (data) + file.unmap(data); + file.close(); + return ""; + } + } + else { + return QString::fromStdString(m_walletImpl->signMessage(message.toStdString())); + } +} + +bool Wallet::verifySignedMessage(const QString &message, const QString &address, const QString &signature, bool filename) const +{ + if (filename) { + QFile file(message); + uchar *data = NULL; + + try { + if (!file.open(QIODevice::ReadOnly)) + return false; + quint64 size = file.size(); + if (size == 0) { + file.close(); + return m_walletImpl->verifySignedMessage(std::string(), address.toStdString(), signature.toStdString()); + } + data = file.map(0, size); + if (!data) { + file.close(); + return false; + } + bool ret = m_walletImpl->verifySignedMessage(std::string((const char*)data, size), address.toStdString(), signature.toStdString()); + file.unmap(data); + file.close(); + return ret; + } + catch (const std::exception &e) { + if (data) + file.unmap(data); + file.close(); + return false; + } + } + else { + return m_walletImpl->verifySignedMessage(message.toStdString(), address.toStdString(), signature.toStdString()); + } +} + Wallet::Wallet(Bitmonero::Wallet *w, QObject *parent) : QObject(parent) , m_walletImpl(w) diff --git a/src/libwalletqt/Wallet.h b/src/libwalletqt/Wallet.h index 458dea90..102429d5 100644 --- a/src/libwalletqt/Wallet.h +++ b/src/libwalletqt/Wallet.h @@ -140,6 +140,12 @@ public: //! integrated address Q_INVOKABLE QString integratedAddress(const QString &paymentId) const; + //! signing a message + Q_INVOKABLE QString signMessage(const QString &message, bool filename = false) const; + + //! verify a signed message + Q_INVOKABLE bool verifySignedMessage(const QString &message, const QString &address, const QString &signature, bool filename = false) const; + //! saved payment id QString paymentId() const;