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;