Skip to content
Snippets Groups Projects
Commit 34365d34 authored by inso's avatar inso
Browse files

Add revocation feature

parent a05f6762
Branches
Tags
No related merge requests found
......@@ -50,6 +50,12 @@
<string>&amp;Open</string>
</property>
</widget>
<widget class="QMenu" name="menuAdvanced">
<property name="title">
<string>Advanced</string>
</property>
<addaction name="action_revoke_identity"/>
</widget>
<addaction name="menu_change_account"/>
<addaction name="action_configure_parameters"/>
<addaction name="action_add_account"/>
......@@ -59,6 +65,8 @@
<addaction name="separator"/>
<addaction name="action_add_a_contact"/>
<addaction name="menu_contacts_list"/>
<addaction name="separator"/>
<addaction name="menuAdvanced"/>
</widget>
<widget class="QMenu" name="menu_help">
<property name="title">
......@@ -188,6 +196,11 @@
<string>&amp;Manage local node</string>
</property>
</action>
<action name="action_revoke_identity">
<property name="text">
<string>Revoke an identity</string>
</property>
</action>
</widget>
<resources>
<include location="../icons/icons.qrc"/>
......
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>RevocationDialog</class>
<widget class="QDialog" name="RevocationDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>250</height>
</rect>
</property>
<property name="windowTitle">
<string>Revoke an identity</string>
</property>
<property name="modal">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QStackedWidget" name="stackedWidget">
<property name="styleSheet">
<string notr="true">QGroupBox {
border: 1px solid gray;
border-radius: 9px;
margin-top: 0.5em;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 10px;
padding: 0 3px 0 3px;
font-weight: bold;
}</string>
</property>
<property name="currentIndex">
<number>1</number>
</property>
<widget class="QWidget" name="page_load_file">
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QLabel" name="label">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>&lt;h2&gt;Select a revokation document&lt;/h1&gt;</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="button_load">
<property name="text">
<string>Load from file</string>
</property>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="styleSheet">
<string notr="true"/>
</property>
<property name="title">
<string>Revocation document</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QLabel" name="label_revocation_content">
<property name="text">
<string/>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="page_destination">
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="QLabel" name="label_2">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-size:x-large; font-weight:600;&quot;&gt;Select publication destination&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QRadioButton" name="radio_community">
<property name="text">
<string>To a co&amp;mmunity</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="combo_community"/>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<property name="sizeConstraint">
<enum>QLayout::SetDefaultConstraint</enum>
</property>
<item>
<widget class="QRadioButton" name="radio_address">
<property name="text">
<string>&amp;To an address</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="edit_address"/>
</item>
<item>
<widget class="QSpinBox" name="spinbox_port">
<property name="maximum">
<number>65535</number>
</property>
<property name="value">
<number>8201</number>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>Revocation information</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<widget class="QLabel" name="label_revocation_info">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>1</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="topMargin">
<number>6</number>
</property>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="button_next">
<property name="text">
<string>Next</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
<slots>
<slot>open_process_add_community()</slot>
<slot>key_changed(int)</slot>
<slot>action_remove_community()</slot>
<slot>open_process_edit_community(QModelIndex)</slot>
<slot>next()</slot>
<slot>previous()</slot>
<slot>open_import_key()</slot>
<slot>open_generate_account_key()</slot>
<slot>action_edit_account_key()</slot>
<slot>action_edit_account_parameters()</slot>
<slot>action_show_pubkey()</slot>
<slot>action_delete_account()</slot>
</slots>
</ui>
......@@ -4,7 +4,7 @@ Created on 1 févr. 2014
@author: inso
"""
from duniterpy.documents import Membership, SelfCertification, Certification, Revokation, BlockUID, Block
from duniterpy.documents import Membership, SelfCertification, Certification, Revocation, BlockUID, Block
from duniterpy.key import SigningKey
from duniterpy.api import bma
from duniterpy.api.bma import PROTOCOL_VERSION
......@@ -546,7 +546,7 @@ class Account(QObject):
"""
revoked = await self._identities_registry.future_find(self.pubkey, community)
revokation = Revokation(PROTOCOL_VERSION, community.currency, None)
revokation = Revocation(PROTOCOL_VERSION, community.currency, None)
selfcert = await revoked.selfcert(community)
key = SigningKey(self.salt, password)
......@@ -578,9 +578,9 @@ class Account(QObject):
:param sakia.core.Community community: the community
:param str password: the password
:return: the revokation document
:rtype: duniterpy.documents.certification.Revokation
:rtype: duniterpy.documents.certification.Revocation
"""
document = Revokation(PROTOCOL_VERSION, community.currency, self.pubkey, "")
document = Revocation(PROTOCOL_VERSION, community.currency, self.pubkey, "")
identity = await self.identity(community)
selfcert = await identity.selfcert(community)
......
......@@ -68,6 +68,9 @@ class Transfer(QObject):
self._locally_created = locally_created
self._metadata = metadata
# Dict containing states of a transfer :
# keys are a tuple containg (current_state, transition_parameters)
# values are tuples containing (transition_test, transition_success, new_state)
self._table_states = {
(TransferState.TO_SEND, (list, Block)):
(
......
......@@ -21,6 +21,7 @@ from .community_view import CommunityWidget
from .contact import ConfigureContactDialog
from .import_account import ImportAccountDialog
from .certification import CertificationDialog
from .revocation import RevocationDialog
from .password_asker import PasswordAskerDialog
from .preferences import PreferencesDialog
from .process_cfg_community import ProcessConfigureCommunity
......@@ -110,6 +111,7 @@ class MainWindow(QObject):
self.ui.actionCertification.triggered.connect(self.open_certification_dialog)
self.ui.actionPreferences.triggered.connect(self.open_preferences_dialog)
self.ui.actionAbout.triggered.connect(self.open_about_popup)
self.ui.action_revoke_identity.triggered.connect(self.open_revocation_dialog)
self.ui.actionManage_local_node.triggered.connect(self.open_duniter_ui)
self.ui.menu_duniter.setDisabled(True)
......@@ -263,6 +265,10 @@ class MainWindow(QObject):
self.community_view.community,
self.password_asker)
def open_revocation_dialog(self):
RevocationDialog.open_dialog(self.app,
self.account)
def open_add_contact_dialog(self):
dialog = ConfigureContactDialog.new_contact(self.app, self.account, self.widget)
dialog.exec_()
......
import asyncio
import aiohttp
import logging
from duniterpy.api import errors
from duniterpy.api import bma
from duniterpy.documents import MalformedDocumentError
from duniterpy.documents.certification import Revocation
from sakia.core.net import Node
from PyQt5.QtWidgets import QDialog, QFileDialog, QMessageBox
from PyQt5.QtCore import QObject
from .widgets.dialogs import QAsyncMessageBox
from ..tools.decorators import asyncify
from ..gen_resources.revocation_uic import Ui_RevocationDialog
class RevocationDialog(QObject):
"""
A dialog to revoke an identity
"""
def __init__(self, app, account, widget, ui):
"""
Constructor if a certification dialog
:param sakia.core.Application app:
:param sakia.core.Account account:
:param PyQt5.QtWidgets widget: the widget of the dialog
:param sakia.gen_resources.revocation_uic.Ui_RevocationDialog ui: the view of the certification dialog
:return:
"""
super().__init__()
self.widget = widget
self.ui = ui
self.ui.setupUi(self.widget)
self.app = app
self.account = account
self.revocation_document = None
self.revoked_selfcert = None
self._steps = (
{
'page': self.ui.page_load_file,
'init': self.init_dialog,
'next': self.revocation_selected
},
{
'page': self.ui.page_destination,
'init': self.init_publication_page,
'next': self.publish
}
)
self._current_step = 0
self.handle_next_step(init=True)
self.ui.button_next.clicked.connect(lambda checked: self.handle_next_step(False))
def handle_next_step(self, init=False):
if self._current_step < len(self._steps) - 1:
if not init:
self.ui.button_next.clicked.disconnect(self._steps[self._current_step]['next'])
self._current_step += 1
self._steps[self._current_step]['init']()
self.ui.stackedWidget.setCurrentWidget(self._steps[self._current_step]['page'])
self.ui.button_next.clicked.connect(self._steps[self._current_step]['next'])
def init_dialog(self):
self.ui.button_next.setEnabled(False)
self.ui.button_load.clicked.connect(self.load_from_file)
self.ui.radio_address.toggled.connect(lambda c: self.publication_mode_changed("address"))
self.ui.radio_community.toggled.connect(lambda c: self.publication_mode_changed("community"))
self.ui.edit_address.textChanged.connect(self.refresh)
self.ui.spinbox_port.valueChanged.connect(self.refresh)
self.ui.combo_community.currentIndexChanged.connect(self.refresh)
def publication_mode_changed(self, radio):
self.ui.edit_address.setEnabled(radio == "address")
self.ui.spinbox_port.setEnabled(radio == "address")
self.ui.combo_community.setEnabled(radio == "community")
self.refresh()
def load_from_file(self):
selected_files = QFileDialog.getOpenFileName(self.widget,
self.tr("Load a revocation file"),
"",
self.tr("All text files (*.txt)"))
selected_file = selected_files[0]
try:
with open(selected_file, 'r') as file:
file_content = file.read()
self.revocation_document = Revocation.from_signed_raw(file_content)
self.revoked_selfcert = Revocation.extract_self_cert(file_content)
self.refresh()
self.ui.button_next.setEnabled(True)
except MalformedDocumentError:
QMessageBox.critical(self.widget, self.tr("Error loading document"),
self.tr("Loaded document is not a revocation document"),
QMessageBox.Ok)
self.ui.button_next.setEnabled(False)
def revocation_selected(self):
pass
def init_publication_page(self):
self.ui.combo_community.clear()
if self.account:
for community in self.account.communities:
self.ui.combo_community.addItem(community.currency)
self.ui.radio_community.setChecked(True)
else:
self.ui.radio_address.setChecked(True)
self.ui.radio_community.setEnabled(False)
def publish(self):
self.ui.button_next.setEnabled(False)
answer = QMessageBox.warning(self.widget, self.tr("Revocation"),
self.tr("""<h4>The publication of this document will remove your identity from the network.</h4>
<li>
<li> <b>This identity won't be able to join the targeted community anymore.</b> </li>
<li> <b>This identity won't be able to generate Universal Dividends anymore.</b> </li>
<li> <b>This identity won't be able to certify individuals anymore.</b> </li>
</li>
Please think twice before publishing this document.
"""), QMessageBox.Ok | QMessageBox.Cancel)
if answer == QMessageBox.Ok:
self.accept()
else:
self.ui.button_next.setEnabled(True)
@asyncify
async def accept(self):
try:
session = aiohttp.ClientSession()
if self.ui.radio_community.isChecked():
community = self.account.communities[self.ui.combo_community.currentIndex()]
await community.bma_access.broadcast(bma.wot.Revoke, {},
{
'revocation': self.revocation_document.signed_raw(self.revoked_selfcert)
})
elif self.ui.radio_address.isChecked():
server = self.ui.edit_address.text()
port = self.ui.spinbox_port.value()
node = await Node.from_address(None, server, port, session=session)
conn_handler = node.endpoint.conn_handler()
await bma.wot.Revoke(conn_handler).post(session,
revocation=self.revocation_document.signed_raw(self.revoked_selfcert))
except (MalformedDocumentError, ValueError, errors.DuniterError,
aiohttp.errors.ClientError, aiohttp.errors.DisconnectedError,
aiohttp.errors.TimeoutError) as e:
await QAsyncMessageBox.critical(self.widget, self.tr("Error broadcasting document"),
str(e))
else:
await QAsyncMessageBox.information(self.widget, self.tr("Revocation broadcast"),
self.tr("The document was successfully broadcasted."))
self.widget.accept()
finally:
session.close()
@classmethod
def open_dialog(cls, app, account):
"""
Certify and identity
:param sakia.core.Application app: the application
:param sakia.core.Account account: the account certifying the identity
:return:
"""
dialog = cls(app, account, QDialog(), Ui_RevocationDialog())
dialog.refresh()
return dialog.exec()
def refresh(self):
if self.revoked_selfcert:
text = self.tr("""
<div>Identity revoked : {uid} (public key : {pubkey}...)</div>
<div>Identity signed on block : {timestamp}</div>
""".format(uid=self.revoked_selfcert.uid,
pubkey=self.revoked_selfcert.pubkey[:12],
timestamp=self.revoked_selfcert.timestamp))
self.ui.label_revocation_content.setText(text)
if self.ui.radio_community.isChecked():
target = self.tr("All nodes of community {name}".format(name=self.ui.combo_community.currentText()))
elif self.ui.radio_address.isChecked():
target = self.tr("Address {address}:{port}".format(address=self.ui.edit_address.text(),
port=self.ui.spinbox_port.value()))
else:
target = ""
self.ui.label_revocation_info.setText("""
<h4>Revocation document</h4>
<div>{text}</div>
<h4>Publication address</h4>
<div>{target}</div>
""".format(text=text,
target=target))
else:
self.ui.label_revocation_content.setText("")
def async_exec(self):
future = asyncio.Future()
self.widget.finished.connect(lambda r: future.set_result(r))
self.widget.open()
self.refresh()
return future
def exec(self):
self.widget.exec()
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment