Welcome to mirror list, hosted at ThFree Co, Russian Federation.

dev.gajim.org/gajim/gajim-plugins.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPhilipp Hörist <forenjunkie@chello.at>2017-04-12 00:49:27 +0300
committerPhilipp Hörist <forenjunkie@chello.at>2018-08-30 00:39:34 +0300
commit7776ae0a5907298539838c2524445858bc00a5b9 (patch)
tree607e7ef998289a5f950e2dfc7d2950d9c6c9e52c /openpgp
parentbd1b120b808458f79c450264ba831db63a65798b (diff)
[openpgp] Inital commit
Diffstat (limited to 'openpgp')
-rw-r--r--openpgp/__init__.py1
-rw-r--r--openpgp/backend/__init__.py0
-rw-r--r--openpgp/backend/gpgme.py196
-rw-r--r--openpgp/backend/pygpg.py183
-rw-r--r--openpgp/backend/sql.py102
-rw-r--r--openpgp/gtk/__init__.py0
-rw-r--r--openpgp/gtk/key.py263
-rw-r--r--openpgp/gtk/style.css17
-rw-r--r--openpgp/gtk/wizard.py253
-rw-r--r--openpgp/manifest.ini8
-rw-r--r--openpgp/modules/__init__.py0
-rw-r--r--openpgp/modules/openpgp.py538
-rw-r--r--openpgp/modules/pgp_keylist.py124
-rw-r--r--openpgp/modules/util.py205
-rw-r--r--openpgp/pgpplugin.py194
15 files changed, 2084 insertions, 0 deletions
diff --git a/openpgp/__init__.py b/openpgp/__init__.py
new file mode 100644
index 0000000..4602461
--- /dev/null
+++ b/openpgp/__init__.py
@@ -0,0 +1 @@
+from .pgpplugin import OpenPGPPlugin
diff --git a/openpgp/backend/__init__.py b/openpgp/backend/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/openpgp/backend/__init__.py
diff --git a/openpgp/backend/gpgme.py b/openpgp/backend/gpgme.py
new file mode 100644
index 0000000..238d256
--- /dev/null
+++ b/openpgp/backend/gpgme.py
@@ -0,0 +1,196 @@
+# Copyright (C) 2018 Philipp Hörist <philipp AT hoerist.com>
+#
+# This file is part of Gajim.
+#
+# Gajim 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; version 3 only.
+#
+# Gajim 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 Gajim. If not, see <http://www.gnu.org/licenses/>.
+
+# XEP-0373: OpenPGP for XMPP
+
+
+import io
+from collections import namedtuple
+import logging
+
+import gpg
+
+from gajim.common import app
+
+KeyringItem = namedtuple('KeyringItem',
+ 'type keyid userid fingerprint')
+
+log = logging.getLogger('gajim.plugin_system.openpgp.pgpme')
+
+
+class PGPContext():
+ def __init__(self, jid, gnuhome):
+ self.context = gpg.Context(home_dir=str(gnuhome))
+ # self.create_new_key()
+ # self.get_key_by_name()
+ # self.get_key_by_fingerprint()
+ self.export_public_key()
+
+ def create_new_key(self):
+ parms = """<GnupgKeyParms format="internal">
+ Key-Type: RSA
+ Key-Length: 2048
+ Subkey-Type: RSA
+ Subkey-Length: 2048
+ Name-Real: Joe Tester
+ Name-Comment: with stupid passphrase
+ Name-Email: test@example.org
+ Passphrase: Crypt0R0cks
+ Expire-Date: 2020-12-31
+ </GnupgKeyParms>
+ """
+
+ with self.context as c:
+ c.set_engine_info(gpg.constants.PROTOCOL_OpenPGP, None, app.gajimpaths['MY_DATA'])
+ c.set_progress_cb(gpg.callbacks.progress_stdout)
+ c.op_genkey(parms, None, None)
+ print("Generated key with fingerprint {0}.".format(
+ c.op_genkey_result().fpr))
+
+ def get_all_keys(self):
+ c = gpg.Context()
+ for key in c.keylist():
+ user = key.uids[0]
+ print("Keys for %s (%s):" % (user.name, user.email))
+ for subkey in key.subkeys:
+ features = []
+ if subkey.can_authenticate:
+ features.append('auth')
+ if subkey.can_certify:
+ features.append('cert')
+ if subkey.can_encrypt:
+ features.append('encrypt')
+ if subkey.can_sign:
+ features.append('sign')
+ print(' %s %s' % (subkey.fpr, ','.join(features)))
+
+ def get_key_by_name(self):
+ c = gpg.Context()
+ for key in c.keylist('john'):
+ print(key.subkeys[0].fpr)
+
+ def get_key_by_fingerprint(self):
+ c = gpg.Context()
+ fingerprint = 'key fingerprint to search for'
+ try:
+ key = c.get_key(fingerprint)
+ print('%s (%s)' % (key.uids[0].name, key.uids[0].email))
+ except gpg.errors.KeyNotFound:
+ print("No key for fingerprint '%s'." % fingerprint)
+
+ def get_secret_key(self):
+ '''
+ Key(can_authenticate=1,
+ can_certify=1,
+ can_encrypt=1,
+ can_sign=1,
+ chain_id=None,
+ disabled=0,
+ expired=0,
+ fpr='7ECE1F88BAFCA37F168E1556A4DBDD1BA55FE3CE',
+ invalid=0,
+ is_qualified=0,
+ issuer_name=None,
+ issuer_serial=None,
+ keylist_mode=1,
+ last_update=0,
+ origin=0,
+ owner_trust=5,
+ protocol=0,
+ revoked=0,
+ secret=1,
+ subkeys=[
+ SubKey(can_authenticate=1,
+ can_certify=1,
+ can_encrypt=1,
+ can_sign=1,
+ card_number=None
+ curve=None,
+ disabled=0,
+ expired=0,
+ expires=0,
+ fpr='7ECE1F88BAFCA37F168E1556A4DBDD1BA55FE3CE',
+ invalid=0,
+ is_cardkey=0,
+ is_de_vs=1,
+ is_qualified=0,
+ keygrip='15BECB77301E4810ABB9CA6A9391158E575DABEC',
+ keyid='A4DBDD1BA55FE3CE',
+ length=2048,
+ pubkey_algo=1,
+ revoked=0,
+ secret=1,
+ timestamp=1525006759)],
+ uids=[
+ UID(address=None,
+ comment='',
+ email='',
+ invalid=0,
+ last_update=0,
+ name='xmpp:philw@jabber.at',
+ origin=0,
+ revoked=0,
+ signatures=[],
+ tofu=[],
+ uid='xmpp:philw@jabber.at',
+ validity=5)])
+ '''
+ for key in self.context.keylist(secret=True):
+ break
+ return key.fpr, key.fpr[-16:]
+
+ def get_keys(self, secret=False):
+ keys = []
+ for key in self.context.keylist():
+ for uid in key.uids:
+ if uid.uid.startswith('xmpp:'):
+ keys.append((key, uid.uid[5:]))
+ break
+ return keys
+
+ def export_public_key(self):
+ # print(dir(self.context))
+ result = self.context.key_export_minimal()
+ print(result)
+
+ def encrypt_decrypt_files(self):
+ c = gpg.Context()
+ recipient = c.get_key("fingerprint of recipient's key")
+
+ # Encrypt
+ with open('foo.txt', 'r') as input_file:
+ with open('foo.txt.gpg', 'wb') as output_file:
+ c.encrypt([recipient], 0, input_file, output_file)
+
+ # Decrypt
+ with open('foo.txt.gpg', 'rb') as input_file:
+ with open('foo2.txt', 'w') as output_file:
+ c.decrypt(input_file, output_file)
+
+ def encrypt(self):
+ c = gpg.Context()
+ recipient = c.get_key("fingerprint of recipient's key")
+
+ plaintext_string = u'plain text data'
+ plaintext_bytes = io.BytesIO(plaintext_string.encode('utf8'))
+ encrypted_bytes = io.BytesIO()
+ c.encrypt([recipient], 0, plaintext_bytes, encrypted_bytes)
+
+ def decrypt(self):
+ c = gpg.Context()
+ decrypted_bytes = io.BytesIO()
+ c.decrypt(encrypted_bytes, decrypted_bytes)
+ decrypted_string = decrypted_bytes.getvalue().decode('utf8')
diff --git a/openpgp/backend/pygpg.py b/openpgp/backend/pygpg.py
new file mode 100644
index 0000000..5b94e0e
--- /dev/null
+++ b/openpgp/backend/pygpg.py
@@ -0,0 +1,183 @@
+# Copyright (C) 2018 Philipp Hörist <philipp AT hoerist.com>
+#
+# This file is part of Gajim.
+#
+# Gajim 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; version 3 only.
+#
+# Gajim 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 Gajim. If not, see <http://www.gnu.org/licenses/>.
+
+# XEP-0373: OpenPGP for XMPP
+
+import os
+import logging
+from collections import namedtuple
+
+import gnupg
+
+from gajim.common import app
+
+from openpgp.modules.util import DecryptionFailed
+
+log = logging.getLogger('gajim.plugin_system.openpgp.pygnupg')
+# gnupg.logger = log
+
+KeyringItem = namedtuple('KeyringItem', 'jid keyid fingerprint')
+
+
+class PGPContext(gnupg.GPG):
+ def __init__(self, jid, gnupghome):
+ gnupg.GPG.__init__(
+ self, gpgbinary=app.get_gpg_binary(), gnupghome=str(gnupghome))
+
+ self._passphrase = 'gajimopenpgppassphrase'
+ self._jid = jid
+ self._own_fingerprint = None
+
+ def _get_key_params(self, jid, passphrase):
+ '''
+ Generate --gen-key input
+ '''
+
+ params = {
+ 'Key-Type': 'RSA',
+ 'Key-Length': 2048,
+ 'Name-Real': 'xmpp:%s' % jid,
+ 'Passphrase': passphrase,
+ }
+
+ out = "Key-Type: %s\n" % params.pop('Key-Type')
+ for key, val in list(params.items()):
+ out += "%s: %s\n" % (key, val)
+ out += "%commit\n"
+ return out
+
+ def generate_key(self):
+ super().gen_key(self._get_key_params(self._jid, self._passphrase))
+
+ def encrypt(self, payload, keys):
+ recipients = [key.fingerprint for key in keys]
+ log.info('encrypt to:')
+ for fingerprint in recipients:
+ log.info(fingerprint)
+
+ result = super().encrypt(str(payload).encode('utf8'),
+ recipients,
+ sign=self._own_fingerprint,
+ always_trust=True,
+ passphrase=self._passphrase)
+
+ if result.ok:
+ error = ''
+ else:
+ error = result.status
+
+ return str(result), error
+
+ def decrypt(self, payload):
+ result = super().decrypt(payload,
+ always_trust=True,
+ passphrase=self._passphrase)
+ if not result.ok:
+ raise DecryptionFailed(result.status)
+
+ return result.data.decode('utf8')
+
+ def get_key(self, fingerprint):
+ return super().list_keys(keys=[fingerprint])
+
+ def get_keys(self, secret=False):
+ result = super().list_keys(secret=secret)
+ keys = []
+ for key in result:
+ item = self._make_keyring_item(key)
+ if item is None:
+ continue
+ keys.append(self._make_keyring_item(key))
+ return keys
+
+ @staticmethod
+ def _make_keyring_item(key):
+ userid = key['uids'][0]
+ if not userid.startswith('xmpp:'):
+ log.warning('Incorrect userid: %s found for key, '
+ 'key will be ignored', userid)
+ return
+ jid = userid[5:]
+ return KeyringItem(jid, key['keyid'], key['fingerprint'])
+
+ def import_key(self, data, jid):
+ log.info('Import key from %s', jid)
+ result = super().import_keys(data)
+ if not result:
+ log.error('Could not import key')
+ log.error(result.results[0])
+ return
+
+ if not self.validate_key(data, jid):
+ return None
+ key = self.get_key(result.results[0]['fingerprint'])
+ return self._make_keyring_item(key[0])
+
+ def validate_key(self, public_key, jid):
+ import tempfile
+ temppath = os.path.join(tempfile.gettempdir(), 'temp_pubkey')
+ with open(temppath, 'wb') as tempfile:
+ tempfile.write(public_key)
+
+ result = self.scan_keys(temppath)
+ if result:
+ for uid in result.uids:
+ if uid.startswith('xmpp:'):
+ if uid[5:] == jid:
+ key_found = True
+ else:
+ log.warning('Found wrong userid in key: %s != %s',
+ uid[5:], jid)
+ log.debug(result)
+ os.remove(temppath)
+ return False
+
+ if not key_found:
+ log.warning('No valid userid found in key')
+ log.debug(result)
+ os.remove(temppath)
+ return False
+
+ log.info('Key validation succesful')
+ os.remove(temppath)
+ return True
+
+ log.warning('Invalid key data: %s')
+ log.debug(result)
+ os.remove(temppath)
+ return False
+
+ def get_own_key_details(self):
+ result = super().list_keys(secret=True)
+ if not result:
+ return None, None
+
+ if len(result) > 1:
+ log.error('More than one secret key found')
+ return None, None
+
+ self._own_fingerprint = result[0]['fingerprint']
+ return self._own_fingerprint, int(result[0]['date'])
+
+ def export_key(self, fingerprint):
+ key = super().export_keys(
+ fingerprint, secret=False, armor=False, minimal=False,
+ passphrase=self._passphrase)
+ return key
+
+ def delete_key(self, fingerprint):
+ log.info('Delete Key: %s', fingerprint)
+ super().delete_keys(fingerprint, passphrase=self._passphrase)
diff --git a/openpgp/backend/sql.py b/openpgp/backend/sql.py
new file mode 100644
index 0000000..564b504
--- /dev/null
+++ b/openpgp/backend/sql.py
@@ -0,0 +1,102 @@
+# Copyright (C) 2018 Philipp Hörist <philipp AT hoerist.com>
+#
+# This file is part of Gajim.
+#
+# Gajim 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; version 3 only.
+#
+# Gajim 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 Gajim. If not, see <http://www.gnu.org/licenses/>.
+
+# XEP-0373: OpenPGP for XMPP
+
+import sqlite3
+import logging
+from collections import namedtuple
+
+log = logging.getLogger('gajim.plugin_system.openpgp.sql')
+
+TABLE_LAYOUT = '''
+ CREATE TABLE contacts (
+ jid TEXT,
+ fingerprint TEXT,
+ active BOOLEAN,
+ trust INTEGER,
+ timestamp INTEGER,
+ comment TEXT
+ );
+ CREATE UNIQUE INDEX jid_fingerprint ON contacts (jid, fingerprint);'''
+
+
+class Storage:
+ def __init__(self, folder_path):
+ self._con = sqlite3.connect(folder_path / 'contacts.db',
+ detect_types=sqlite3.PARSE_DECLTYPES)
+ self._con.row_factory = self._namedtuple_factory
+ self._create_database()
+ self._migrate_database()
+ self._con.execute("PRAGMA synchronous=FULL;")
+ self._con.commit()
+
+ @staticmethod
+ def _namedtuple_factory(cursor, row):
+ fields = [col[0] for col in cursor.description]
+ Row = namedtuple("Row", fields)
+ named_row = Row(*row)
+ return named_row
+
+ def _user_version(self):
+ return self._con.execute('PRAGMA user_version').fetchone()[0]
+
+ def _create_database(self):
+ if not self._user_version():
+ log.info('Create contacts.db')
+ self._execute_query(TABLE_LAYOUT)
+
+ def _execute_query(self, query):
+ transaction = """
+ BEGIN TRANSACTION;
+ %s
+ PRAGMA user_version=1;
+ END TRANSACTION;
+ """ % (query)
+ self._con.executescript(transaction)
+
+ def _migrate_database(self):
+ pass
+
+ def load_contacts(self):
+ sql = 'SELECT * from contacts'
+ rows = self._con.execute(sql).fetchall()
+ if rows is not None:
+ return rows
+
+ def save_contact(self, db_values):
+ sql = '''REPLACE INTO
+ contacts(jid, fingerprint, active, trust, timestamp, comment)
+ VALUES(?, ?, ?, ?, ?, ?)'''
+ for values in db_values:
+ log.info('Store key: %s', values)
+ self._con.execute(sql, values)
+ self._con.commit()
+
+ def set_trust(self, jid, fingerprint, trust):
+ sql = 'UPDATE contacts SET trust = ? WHERE jid = ? AND fingerprint = ?'
+ log.info('Set Trust: %s %s %s', trust, jid, fingerprint)
+ self._con.execute(sql, (trust, jid, fingerprint))
+ self._con.commit()
+
+ def delete_key(self, jid, fingerprint):
+ sql = 'DELETE from contacts WHERE jid = ? AND fingerprint = ?'
+ log.info('Delete Key: %s %s', jid, fingerprint)
+ self._con.execute(sql, (jid, fingerprint))
+ self._con.commit()
+
+ def cleanup(self):
+ self._con.close()
diff --git a/openpgp/gtk/__init__.py b/openpgp/gtk/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/openpgp/gtk/__init__.py
diff --git a/openpgp/gtk/key.py b/openpgp/gtk/key.py
new file mode 100644
index 0000000..902e35f
--- /dev/null
+++ b/openpgp/gtk/key.py
@@ -0,0 +1,263 @@
+# Copyright (C) 2018 Philipp Hörist <philipp AT hoerist.com>
+#
+# This file is part of Gajim.
+#
+# Gajim 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; version 3 only.
+#
+# Gajim 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 Gajim. If not, see <http://www.gnu.org/licenses/>.
+
+# XEP-0373: OpenPGP for XMPP
+
+import logging
+import time
+
+from gi.repository import Gtk
+
+from gajim.common import app
+from gajim.common.const import DialogButton, ButtonAction
+
+from gajim.gtk import NewConfirmationDialog
+
+from openpgp.modules.util import Trust
+
+log = logging.getLogger('gajim.plugin_system.openpgp.keydialog')
+
+TRUST_DATA = {
+ Trust.NOT_TRUSTED: ('dialog-error-symbolic',
+ _('Not Trusted'),
+ 'error-color'),
+ Trust.UNKNOWN: ('security-low-symbolic',
+ _('Not Decided'),
+ 'warning-color'),
+ Trust.BLIND: ('security-medium-symbolic',
+ _('Blind Trust'),
+ 'openpgp-dark-success-color'),
+ Trust.VERIFIED: ('security-high-symbolic',
+ _('Verified'),
+ 'success-color')
+}
+
+
+class KeyDialog(Gtk.Dialog):
+ def __init__(self, account, jid, transient):
+ flags = Gtk.DialogFlags.DESTROY_WITH_PARENT
+ super().__init__(_('Public Keys for %s') % jid, None, flags)
+
+ self.set_transient_for(transient)
+ self.set_resizable(True)
+ self.set_default_size(500, 300)
+
+ self.get_style_context().add_class('openpgp-key-dialog')
+
+ self.con = app.connections[account]
+
+ self._listbox = Gtk.ListBox()
+ self._listbox.set_selection_mode(Gtk.SelectionMode.NONE)
+
+ self._scrolled = Gtk.ScrolledWindow()
+ self._scrolled.set_policy(Gtk.PolicyType.NEVER,
+ Gtk.PolicyType.AUTOMATIC)
+ self._scrolled.add(self._listbox)
+
+ box = self.get_content_area()
+ box.pack_start(self._scrolled, True, True, 0)
+
+ keys = self.con.get_module('OpenPGP').get_keys(jid, only_trusted=False)
+ for key in keys:
+ log.info('Load: %s', key.fingerprint)
+ self._listbox.add(KeyRow(key))
+ self.show_all()
+
+
+class KeyRow(Gtk.ListBoxRow):
+ def __init__(self, key):
+ Gtk.ListBoxRow.__init__(self)
+ self.set_activatable(False)
+
+ self._dialog = self.get_toplevel()
+ self.key = key
+
+ box = Gtk.Box()
+ box.set_spacing(12)
+
+ self._trust_button = TrustButton(self)
+ box.add(self._trust_button)
+
+ label_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
+ fingerprint = Gtk.Label(
+ label=self._format_fingerprint(key.fingerprint))
+ fingerprint.get_style_context().add_class('openpgp-mono')
+ if not key.active:
+ fingerprint.get_style_context().add_class('openpgp-inactive-color')
+ fingerprint.set_selectable(True)
+ fingerprint.set_halign(Gtk.Align.START)
+ fingerprint.set_valign(Gtk.Align.START)
+ fingerprint.set_hexpand(True)
+ label_box.add(fingerprint)
+
+ date = Gtk.Label(label=self._format_timestamp(key.timestamp))
+ date.set_halign(Gtk.Align.START)
+ date.get_style_context().add_class('openpgp-mono')
+ if not key.active:
+ date.get_style_context().add_class('openpgp-inactive-color')
+ label_box.add(date)
+
+ box.add(label_box)
+
+ self.add(box)
+ self.show_all()
+
+ def delete_fingerprint(self, *args):
+ def _remove():
+ self.get_parent().remove(self)
+ self.key.delete()
+ self.destroy()
+
+ buttons = {
+ Gtk.ResponseType.CANCEL: DialogButton('Cancel'),
+ Gtk.ResponseType.OK: DialogButton('Delete',
+ _remove,
+ ButtonAction.DESTRUCTIVE),
+ }
+
+ NewConfirmationDialog(
+ _('Delete Public Key'),
+ _('This will permanently delete this public key'),
+ buttons,
+ transient_for=self.get_toplevel())
+
+ def set_trust(self, trust):
+ icon_name, tooltip, css_class = TRUST_DATA[trust]
+ image = self._trust_button.get_child()
+ image.set_from_icon_name(icon_name, Gtk.IconSize.MENU)
+ image.get_style_context().add_class(css_class)
+
+ @staticmethod
+ def _format_fingerprint(fingerprint):
+ fplen = len(fingerprint)
+ wordsize = fplen // 8
+ buf = ''
+ for w in range(0, fplen, wordsize):
+ buf += '{0} '.format(fingerprint[w:w + wordsize])
+ return buf.rstrip()
+
+ @staticmethod
+ def _format_timestamp(timestamp):
+ return time.strftime('%Y-%m-%d %H:%M:%S',
+ time.localtime(timestamp))
+
+
+class TrustButton(Gtk.MenuButton):
+ def __init__(self, row):
+ Gtk.MenuButton.__init__(self)
+ self._row = row
+ self._css_class = ''
+ self.set_popover(TrustPopver(row))
+ self.update()
+
+ def update(self):
+ icon_name, tooltip, css_class = TRUST_DATA[self._row.key.trust]
+ image = self.get_child()
+ image.set_from_icon_name(icon_name, Gtk.IconSize.MENU)
+ # remove old color from icon
+ image.get_style_context().remove_class(self._css_class)
+
+ if not self._row.key.active:
+ css_class = 'openpgp-inactive-color'
+ tooltip = '%s - %s' % (_('Inactive'), tooltip)
+
+ image.get_style_context().add_class(css_class)
+ self._css_class = css_class
+ self.set_tooltip_text(tooltip)
+
+
+class TrustPopver(Gtk.Popover):
+ def __init__(self, row):
+ Gtk.Popover.__init__(self)
+ self._row = row
+ self._listbox = Gtk.ListBox()
+ self._listbox.set_selection_mode(Gtk.SelectionMode.NONE)
+ if row.key.trust != Trust.VERIFIED:
+ self._listbox.add(VerifiedOption())
+ if row.key.trust != Trust.NOT_TRUSTED:
+ self._listbox.add(NotTrustedOption())
+ self._listbox.add(DeleteOption())
+ self.add(self._listbox)
+ self._listbox.show_all()
+ self._listbox.connect('row-activated', self._activated)
+ self.get_style_context().add_class('openpgp-trust-popover')
+
+ def _activated(self, listbox, row):
+ self.popdown()
+ if row.type_ is None:
+ self._row.delete_fingerprint()
+ else:
+ self._row.key.trust = row.type_
+ self.get_relative_to().update()
+ self.update()
+
+ def update(self):
+ self._listbox.foreach(lambda row: self._listbox.remove(row))
+ if self._row.key.trust != Trust.VERIFIED:
+ self._listbox.add(VerifiedOption())
+ if self._row.key.trust != Trust.NOT_TRUSTED:
+ self._listbox.add(NotTrustedOption())
+ self._listbox.add(DeleteOption())
+
+
+class MenuOption(Gtk.ListBoxRow):
+ def __init__(self):
+ Gtk.ListBoxRow.__init__(self)
+ box = Gtk.Box()
+ box.set_spacing(6)
+
+ image = Gtk.Image.new_from_icon_name(self.icon,
+ Gtk.IconSize.MENU)
+ label = Gtk.Label(label=self.label)
+ image.get_style_context().add_class(self.color)
+
+ box.add(image)
+ box.add(label)
+ self.add(box)
+ self.show_all()
+
+
+class VerifiedOption(MenuOption):
+
+ type_ = Trust.VERIFIED
+ icon = 'security-high-symbolic'
+ label = _('Verified')
+ color = 'success-color'
+
+ def __init__(self):
+ MenuOption.__init__(self)
+
+
+class NotTrustedOption(MenuOption):
+
+ type_ = Trust.NOT_TRUSTED
+ icon = 'dialog-error-symbolic'
+ label = _('Not Trusted')
+ color = 'error-color'
+
+ def __init__(self):
+ MenuOption.__init__(self)
+
+
+class DeleteOption(MenuOption):
+
+ type_ = None
+ icon = 'user-trash-symbolic'
+ label = _('Delete')
+ color = ''
+
+ def __init__(self):
+ MenuOption.__init__(self)
diff --git a/openpgp/gtk/style.css b/openpgp/gtk/style.css
new file mode 100644
index 0000000..ccdcd96
--- /dev/null
+++ b/openpgp/gtk/style.css
@@ -0,0 +1,17 @@
+.openpgp-dark-success-color { color: darker(@success_color); }
+.openpgp-inactive-color { color: @unfocused_borders; }
+
+.openpgp-mono { font-size: 12px; font-family: monospace; }
+
+.openpgp-key-dialog > box { margin: 12px; }
+
+.openpgp-key-dialog scrolledwindow row {
+ border-bottom: 1px solid;
+ border-color: @unfocused_borders;
+ padding: 10px 20px 10px 10px;
+}
+.openpgp-key-dialog scrolledwindow row:last-child { border-bottom: 0px}
+
+.openpgp-key-dialog scrolledwindow { border: 1px solid; border-color:@unfocused_borders; }
+
+.openpgp-trust-popover row { padding: 10px 15px 10px 10px; }
diff --git a/openpgp/gtk/wizard.py b/openpgp/gtk/wizard.py
new file mode 100644
index 0000000..7213143
--- /dev/null
+++ b/openpgp/gtk/wizard.py
@@ -0,0 +1,253 @@
+# Copyright (C) 2018 Philipp Hörist <philipp AT hoerist.com>
+#
+# This file is part of Gajim.
+#
+# Gajim 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; version 3 only.
+#
+# Gajim 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 Gajim. If not, see <http://www.gnu.org/licenses/>.
+
+# XEP-0373: OpenPGP for XMPP
+
+import logging
+import threading
+from enum import IntEnum
+
+from gi.repository import Gtk
+from gi.repository import GLib
+
+from gajim.common import app
+
+log = logging.getLogger('gajim.plugin_system.openpgp.wizard')
+
+
+class Page(IntEnum):
+ WELCOME = 0
+ NEWKEY = 1
+ SUCCESS = 2
+ ERROR = 3
+
+
+class KeyWizard(Gtk.Assistant):
+ def __init__(self, plugin, account, chat_control):
+ Gtk.Assistant.__init__(self)
+
+ self._con = app.connections[account]
+ self._plugin = plugin
+ self._account = account
+ self._data_form_widget = None
+ self._is_form = None
+ self._chat_control = chat_control
+
+ self.set_application(app.app)
+ self.set_transient_for(chat_control.parent_win.window)
+ self.set_resizable(True)
+ self.set_position(Gtk.WindowPosition.CENTER)
+
+ self.set_default_size(600, 400)
+ self.get_style_context().add_class('dialog-margin')
+
+ self._add_page(WelcomePage())
+ # self._add_page(BackupKeyPage())
+ self._add_page(NewKeyPage(self, self._con))
+ # self._add_page(SaveBackupCodePage())
+ self._add_page(SuccessfulPage())
+ self._add_page(ErrorPage())
+
+ self.connect('prepare', self._on_page_change)
+ self.connect('cancel', self._on_cancel)
+ self.connect('close', self._on_cancel)
+
+ self._remove_sidebar()
+ self.show_all()
+
+ def _add_page(self, page):
+ self.append_page(page)
+ self.set_page_type(page, page.type_)
+ self.set_page_title(page, page.title)
+ self.set_page_complete(page, page.complete)
+
+ def _remove_sidebar(self):
+ main_box = self.get_children()[0]
+ sidebar = main_box.get_children()[0]
+ main_box.remove(sidebar)
+
+ def _activate_encryption(self):
+ win = self._chat_control.parent_win.window
+ action = win.lookup_action(
+ 'set-encryption-%s' % self._chat_control.control_id)
+ action.activate(GLib.Variant("s", self._plugin.encryption_name))
+
+ def _on_page_change(self, assistant, page):
+ if self.get_current_page() == Page.NEWKEY:
+ if self._con.get_module('OpenPGP').secret_key_available:
+ self.set_current_page(Page.SUCCESS)
+ else:
+ page.generate()
+ elif self.get_current_page() == Page.SUCCESS:
+ self._activate_encryption()
+
+ def _on_error(self, error_text):
+ log.info('Show Error page')
+ page = self.get_nth_page(Page.ERROR)
+ page.set_text(error_text)
+ self.set_current_page(Page.ERROR)
+
+ def _on_cancel(self, widget):
+ self.destroy()
+
+
+class WelcomePage(Gtk.Box):
+
+ type_ = Gtk.AssistantPageType.INTRO
+ title = _('Welcome')
+ complete = True
+
+ def __init__(self):
+ super().__init__(orientation=Gtk.Orientation.VERTICAL)
+ self.set_spacing(18)
+ title_label = Gtk.Label(label=_('Setup OpenPGP'))
+ text_label = Gtk.Label(
+ label=_('Gajim will now try to setup OpenPGP for you'))
+ self.add(title_label)
+ self.add(text_label)
+
+
+class RequestPage(Gtk.Box):
+
+ type_ = Gtk.AssistantPageType.INTRO
+ title = _('Request OpenPGP Key')
+ complete = False
+
+ def __init__(self):
+ super().__init__(orientation=Gtk.Orientation.VERTICAL)
+ self.set_spacing(18)
+ spinner = Gtk.Spinner()
+ self.pack_start(spinner, True, True, 0)
+ spinner.start()
+
+
+# class BackupKeyPage(Gtk.Box):
+
+# type_ = Gtk.AssistantPageType.INTRO
+# title = _('Supply Backup Code')
+# complete = True
+
+# def __init__(self):
+# super().__init__(orientation=Gtk.Orientation.VERTICAL)
+# self.set_spacing(18)
+# title_label = Gtk.Label(label=_('Backup Code'))
+# text_label = Gtk.Label(
+# label=_('We found a backup Code, please supply your password'))
+# self.add(title_label)
+# self.add(text_label)
+# entry = Gtk.Entry()
+# self.add(entry)
+
+
+class NewKeyPage(RequestPage):
+
+ type_ = Gtk.AssistantPageType.PROGRESS
+ title = _('Generating new Key')
+ complete = False
+
+ def __init__(self, assistant, con):
+ super().__init__()
+ self._assistant = assistant
+ self._con = con
+
+ def generate(self):
+ log.info('Creating Key')
+ thread = threading.Thread(target=self.worker)
+ thread.start()
+
+ def worker(self):
+ error = None
+ try:
+ self._con.get_module('OpenPGP').generate_key()
+ except Exception as e:
+ error = e
+ else:
+ self._con.get_module('OpenPGP').get_own_key_details()
+ self._con.get_module('OpenPGP').publish_key()
+ self._con.get_module('OpenPGP').query_key_list()
+ GLib.idle_add(self.finished, error)
+
+ def finished(self, error):
+ if error is None:
+ self._assistant.set_current_page(Page.SUCCESS)
+ else:
+ log.error(error)
+ self._assistant.set_current_page(Page.ERROR)
+
+
+# class SaveBackupCodePage(RequestPage):
+
+# type_ = Gtk.AssistantPageType.PROGRESS
+# title = _('Save this code')
+# complete = False
+
+# def __init__(self):
+# super().__init__(orientation=Gtk.Orientation.VERTICAL)
+# self.set_spacing(18)
+# title_label = Gtk.Label(label=_('Backup Code'))
+# text_label = Gtk.Label(
+# label=_('This is your backup code, you need it if you reinstall Gajim'))
+# self.add(title_label)
+# self.add(text_label)
+
+
+class SuccessfulPage(Gtk.Box):
+
+ type_ = Gtk.AssistantPageType.SUMMARY
+ title = _('Setup successful')
+ complete = True
+
+ def __init__(self):
+ super().__init__(orientation=Gtk.Orientation.VERTICAL)
+ self.set_spacing(12)
+ self.set_homogeneous(True)
+
+ icon = Gtk.Image.new_from_icon_name('object-select-symbolic',
+ Gtk.IconSize.DIALOG)
+ icon.get_style_context().add_class('success-color')
+ icon.set_valign(Gtk.Align.END)
+ label = Gtk.Label(label=_('Setup successful'))
+ label.get_style_context().add_class('bold16')
+ label.set_valign(Gtk.Align.START)
+
+ self.add(icon)
+ self.add(label)
+
+
+class ErrorPage(Gtk.Box):
+
+ type_ = Gtk.AssistantPageType.SUMMARY
+ title = _('Registration failed')
+ complete = True
+
+ def __init__(self):
+ super().__init__(orientation=Gtk.Orientation.VERTICAL)
+ self.set_spacing(12)
+ self.set_homogeneous(True)
+
+ icon = Gtk.Image.new_from_icon_name('dialog-error-symbolic',
+ Gtk.IconSize.DIALOG)
+ icon.get_style_context().add_class('error-color')
+ icon.set_valign(Gtk.Align.END)
+ self._label = Gtk.Label()
+ self._label.get_style_context().add_class('bold16')
+ self._label.set_valign(Gtk.Align.START)
+
+ self.add(icon)
+ self.add(self._label)
+
+ def set_text(self, text):
+ self._label.set_text(text)
diff --git a/openpgp/manifest.ini b/openpgp/manifest.ini
new file mode 100644
index 0000000..d50716b
--- /dev/null
+++ b/openpgp/manifest.ini
@@ -0,0 +1,8 @@
+[info]
+name: OpenPGP
+short_name: openpgp
+version: 0.90.0
+description: Experimental OpenPGP XEP-0373 Implementation
+authors: Philipp Hörist <philipp@hoerist.com>
+homepage: https://dev.gajim.org/gajim/gajim-plugins/wikis/pgp
+min_gajim_version: 1.0.99
diff --git a/openpgp/modules/__init__.py b/openpgp/modules/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/openpgp/modules/__init__.py
diff --git a/openpgp/modules/openpgp.py b/openpgp/modules/openpgp.py
new file mode 100644
index 0000000..2101f64
--- /dev/null
+++ b/openpgp/modules/openpgp.py
@@ -0,0 +1,538 @@
+# Copyright (C) 2018 Philipp Hörist <philipp AT hoerist.com>
+#
+# This file is part of Gajim.
+#
+# Gajim 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; version 3 only.
+#
+# Gajim 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 Gajim. If not, see <http://www.gnu.org/licenses/>.
+
+# XEP-0373: OpenPGP for XMPP
+
+import time
+import logging
+from pathlib import Path
+from base64 import b64decode, b64encode
+
+from nbxmpp import Node, isResultNode
+
+from gajim.common import app
+from gajim.common import configpaths
+from gajim.common.connection_handlers_events import MessageNotSentEvent
+
+from openpgp.modules import util
+from openpgp.modules.util import NS_OPENPGP_PUBLIC_KEYS
+from openpgp.modules.util import NS_OPENPGP
+from openpgp.modules.util import Key
+from openpgp.modules.util import Trust
+from openpgp.modules.util import VerifyFailed
+from openpgp.modules.util import DecryptionFailed
+from openpgp.backend.sql import Storage
+from openpgp.backend.pygpg import PGPContext
+
+
+log = logging.getLogger('gajim.plugin_system.openpgp')
+
+
+ENCRYPTION_NAME = 'OpenPGP'
+
+# Module name
+name = 'OpenPGP'
+zeroconf = False
+
+
+class KeyData:
+ '''
+ Holds all data related to a certain key
+ '''
+ def __init__(self, contact_data):
+ self._contact_data = contact_data
+ self.fingerprint = None
+ self.active = False
+ self._trust = Trust.UNKNOWN
+ self.timestamp = None
+ self.comment = None
+ self.has_pubkey = False
+
+ @property
+ def trust(self):
+ return self._trust
+
+ @trust.setter
+ def trust(self, value):
+ if value not in (Trust.NOT_TRUSTED,
+ Trust.UNKNOWN,
+ Trust.BLIND,
+ Trust.VERIFIED):
+ raise ValueError('Trust value not allowed: %s' % value)
+ self._trust = value
+ self._contact_data.set_trust(self.fingerprint, self._trust)
+
+ @classmethod
+ def from_key(cls, contact_data, key, trust):
+ keydata = cls(contact_data)
+ keydata.fingerprint = key.fingerprint
+ keydata.timestamp = key.date
+ keydata.active = True
+ keydata._trust = trust
+ return keydata
+
+ @classmethod
+ def from_row(cls, contact_data, row):
+ keydata = cls(contact_data)
+ keydata.fingerprint = row.fingerprint
+ keydata.timestamp = row.timestamp
+ keydata.comment = row.comment
+ keydata._trust = row.trust
+ keydata.active = row.active
+ return keydata
+
+ def delete(self):
+ self._contact_data.delete_key(self.fingerprint)
+
+
+class ContactData:
+ '''
+ Holds all data related to a contact
+ '''
+ def __init__(self, jid, storage, pgp):
+ self.jid = jid
+ self._key_store = {}
+ self._storage = storage
+ self._pgp = pgp
+
+ @property
+ def userid(self):
+ if self._jid is None:
+ raise ValueError('JID not set')
+ return 'xmpp:%s' % self._jid
+
+ @property
+ def default_trust(self):
+ for key in self._key_store:
+ if key.trust in (Trust.NOT_TRUSTED, Trust.BLIND):
+ return Trust.UNKNOWN
+ return Trust.BLIND
+
+ def db_values(self):
+ for key in self._key_store.values():
+ yield (self.jid,
+ key.fingerprint,
+ key.active,
+ key.trust,
+ key.timestamp,
+ key.comment)
+
+ def add_from_key(self, key):
+ try:
+ keydata = self._key_store[key.fingerprint]
+ except KeyError:
+ keydata = KeyData.from_key(self, key, self.default_trust)
+ self._key_store[key.fingerprint] = keydata
+ log.info('Add from key: %s %s', self.jid, keydata.fingerprint)
+ return keydata
+
+ def add_from_db(self, row):
+ try:
+ keydata = self._key_store[row.fingerprint]
+ except KeyError:
+ keydata = KeyData.from_row(self, row)
+ self._key_store[row.fingerprint] = keydata
+ log.info('Add from row: %s %s', self.jid, row.fingerprint)
+ return keydata
+
+ def process_keylist(self, keylist):
+ log.info('Process keylist: %s %s', self.jid, keylist)
+
+ if keylist is None:
+ for keydata in self._key_store.values():
+ keydata.active = False
+ self._storage.save_contact(self.db_values())
+ return []
+
+ missing_pub_keys = []
+ fingerprints = set([key.fingerprint for key in keylist])
+ if fingerprints == self._key_store.keys():
+ log.info('No updates found')
+ for key in self._key_store.values():
+ if not key.has_pubkey:
+ missing_pub_keys.append(key.fingerprint)
+ return missing_pub_keys
+
+ for keydata in self._key_store.values():
+ keydata.active = False
+
+ for key in keylist:
+ try:
+ keydata = self._key_store[key.fingerprint]
+ keydata.active = True
+ if not keydata.has_pubkey:
+ missing_pub_keys.append(keydata.fingerprint)
+ except KeyError:
+ keydata = self.add_from_key(key)
+ missing_pub_keys.append(keydata.fingerprint)
+
+ self._storage.save_contact(self.db_values())
+ return missing_pub_keys
+
+ def set_public_key(self, fingerprint):
+ try:
+ keydata = self._key_store[fingerprint]
+ except KeyError:
+ log.warning('Set public key on unknown fingerprint',
+ self.jid, fingerprint)
+ else:
+ keydata.has_pubkey = True
+ log.info('Set public key: %s %s', self.jid, fingerprint)
+
+ def get_keys(self, only_trusted=True):
+ keys = list(self._key_store.values())
+ if not only_trusted:
+ return keys
+ return [k for k in keys if k.active and k.trust in (Trust.VERIFIED,
+ Trust.BLIND)]
+
+ def set_trust(self, fingerprint, trust):
+ self._storage.set_trust(self.jid, fingerprint, trust)
+
+ def delete_key(self, fingerprint):
+ self._storage.delete_key(self.jid, fingerprint)
+ self._pgp.delete_key(fingerprint)
+ del self._key_store[fingerprint]
+
+
+class PGPContacts:
+ '''
+ Holds all contacts available for PGP encryption
+ '''
+ def __init__(self, pgp, storage):
+ self._contacts = {}
+ self._storage = storage
+ self._pgp = pgp
+ self._load_from_storage()
+ self._load_from_keyring()
+
+ def _load_from_keyring(self):
+ log.info('Load keys from keyring')
+ keyring = self._pgp.get_keys()
+ for key in keyring:
+ log.info('Found: %s %s', key.jid, key.fingerprint)
+ self.set_public_key(key.jid, key.fingerprint)
+
+ def _load_from_storage(self):
+ log.info('Load contacts from storage')
+ rows = self._storage.load_contacts()
+ if rows is None:
+ return
+
+ for row in rows:
+ log.info('Found: %s %s', row.jid, row.fingerprint)
+ try:
+ contact_data = self._contacts[row.jid]
+ except KeyError:
+ contact_data = ContactData(row.jid, self._storage, self._pgp)
+ contact_data.add_from_db(row)
+ self._contacts[row.jid] = contact_data
+ else:
+ contact_data.add_from_db(row)
+
+ def process_keylist(self, jid, keylist):
+ try:
+ contact_data = self._contacts[jid]
+ except KeyError:
+ contact_data = ContactData(jid, self._storage, self._pgp)
+ missing_pub_keys = contact_data.process_keylist(keylist)
+ self._contacts[jid] = contact_data
+ else:
+ missing_pub_keys = contact_data.process_keylist(keylist)
+
+ return missing_pub_keys
+
+ def set_public_key(self, jid, fingerprint):
+ try:
+ contact_data = self._contacts[jid]
+ except KeyError:
+ log.warning('ContactData not found: %s %s', jid, fingerprint)
+ else:
+ contact_data.set_public_key(fingerprint)
+
+ def get_keys(self, jid, only_trusted=True):
+ try:
+ contact_data = self._contacts[jid]
+ return contact_data.get_keys(only_trusted=only_trusted)
+ except KeyError:
+ return []
+
+
+class OpenPGP:
+ def __init__(self, con):
+ self._con = con
+ self._account = con.name
+
+ self.handlers = []
+
+ self.own_jid = self.get_own_jid(stripped=True)
+
+ path = Path(configpaths.get('MY_DATA')) / 'openpgp' / self.own_jid
+ if not path.exists():
+ path.mkdir(parents=True)
+
+ self._pgp = PGPContext(self.own_jid, path)
+ self._storage = Storage(path)
+ self._contacts = PGPContacts(self._pgp, self._storage)
+ self._fingerprint, self._date = self.get_own_key_details()
+ log.info('Own Fingerprint at start: %s', self._fingerprint)
+
+ @property
+ def secret_key_available(self):
+ return self._fingerprint is not None
+
+ def get_own_jid(self, stripped=False):
+ if stripped:
+ return self._con.get_own_jid().getStripped()
+ return self._con.get_own_jid()
+
+ def get_own_key_details(self):
+ self._fingerprint, self._date = self._pgp.get_own_key_details()
+ return self._fingerprint, self._date
+
+ def generate_key(self):
+ self._pgp.generate_key()
+
+ def publish_key(self):
+ log.info('%s => Publish key', self._account)
+ key = self._pgp.export_key(self._fingerprint)
+
+ date = time.strftime(
+ '%Y-%m-%dT%H:%M:%SZ', time.gmtime(self._date))
+ pubkey_node = Node('pubkey', attrs={'xmlns': NS_OPENPGP,
+ 'date': date})
+ data = pubkey_node.addChild('data')
+ data.addData(b64encode(key).decode('utf8'))
+ node = '%s:%s' % (NS_OPENPGP_PUBLIC_KEYS, self._fingerprint)
+
+ self._con.get_module('PubSub').send_pb_publish(
+ self.own_jid, node, pubkey_node,
+ id_='current', cb=self._public_result)
+
+ def _publish_key_list(self, keylist=None):
+ if keylist is None:
+ keylist = [Key(self._fingerprint, self._date)]
+ log.info('%s => Publish keys list', self._account)
+ self._con.get_module('PGPKeylist').send(keylist)
+
+ def _public_result(self, con, stanza):
+ if not isResultNode(stanza):
+ log.error('%s => Publishing failed: %s',
+ self._account, stanza.getError())
+
+ def _query_public_key(self, jid, fingerprint):
+ log.info('%s => Fetch public key %s - %s',
+ self._account, fingerprint, jid)
+ node = '%s:%s' % (NS_OPENPGP_PUBLIC_KEYS, fingerprint)
+ self._con.get_module('PubSub').send_pb_retrieve(
+ jid, node, cb=self._public_key_received, fingerprint=fingerprint)
+
+ def _public_key_received(self, con, stanza, fingerprint):
+ if not isResultNode(stanza):
+ log.error('%s => Public Key not found: %s',
+ self._account, stanza.getError())
+ return
+ pubkey = util.unpack_public_key(stanza, fingerprint)
+ if pubkey is None:
+ log.warning('Invalid public key received:\n%s', stanza)
+ return
+
+ jid = stanza.getFrom().getStripped()
+ result = self._pgp.import_key(pubkey, jid)
+ if result is not None:
+ self._contacts.set_public_key(jid, fingerprint)
+
+ def query_key_list(self, jid=None):
+ if jid is None:
+ jid = self.own_jid
+ log.info('%s => Fetch keys list %s', self._account, jid)
+ self._con.get_module('PubSub').send_pb_retrieve(
+ jid, NS_OPENPGP_PUBLIC_KEYS,
+ cb=self._query_key_list_result)
+
+ def _query_key_list_result(self, con, stanza):
+ from_jid = stanza.getFrom()
+ if from_jid is None:
+ from_jid = self.own_jid
+ else:
+ from_jid = from_jid.getStripped()
+
+ if not isResultNode(stanza):
+ log.error('%s => Keys list query failed: %s',
+ self._account, stanza.getError())
+ if from_jid == self.own_jid and self._fingerprint is not None:
+ self._publish_key_list()
+ return
+
+ from_jid = stanza.getFrom()
+ if from_jid is None:
+ from_jid = self.own_jid
+ else:
+ from_jid = from_jid.getStripped()
+
+ keylist = util.unpack_public_key_list(stanza, from_jid)
+ self.key_list_received(keylist, from_jid)
+
+ def key_list_received(self, keylist, from_jid):
+ if keylist is None:
+ log.warning('Invalid keys list received')
+ if from_jid == self.own_jid and self._fingerprint is not None:
+ self._publish_key_list()
+ return
+
+ if not keylist:
+ log.warning('%s => Empty keys list received from %s',
+ self._account, from_jid)
+ self._contacts.process_keylist(self.own_jid, keylist)
+ if from_jid == self.own_jid and self._fingerprint is not None:
+ self._publish_key_list()
+ return
+
+ if from_jid == self.own_jid:
+ log.info('Received own keys list')
+ for key in keylist:
+ log.info(key.fingerprint)
+ for key in keylist:
+ # Check if own fingerprint is published
+ if key.fingerprint == self._fingerprint:
+ log.info('Own key found in keys list')
+ return
+ log.info('Own key not published')
+ if self._fingerprint is not None:
+ keylist.append(Key(self._fingerprint, None))
+ self._publish_key_list(keylist)
+ return
+
+ missing_pub_keys = self._contacts.process_keylist(from_jid, keylist)
+
+ for key in keylist:
+ log.info(key.fingerprint)
+
+ for fingerprint in missing_pub_keys:
+ self._query_public_key(from_jid, fingerprint)
+
+ def decrypt_message(self, obj, callback):
+ if obj.encrypted:
+ # Another Plugin already decrypted the message
+ return
+
+ if obj.name == 'message-received':
+ enc_tag = obj.stanza.getTag('openpgp', namespace=NS_OPENPGP)
+ jid = obj.jid
+ else:
+ enc_tag = obj.message.getTag('openpgp', namespace=NS_OPENPGP)
+ jid = obj.with_
+
+ if enc_tag is None:
+ return
+
+ log.info('Received OpenPGP message from: %s', jid)
+ b64encode_payload = enc_tag.getData()
+ encrypted_payload = b64decode(b64encode_payload)
+
+ try:
+ decrypted_payload = self._pgp.decrypt(encrypted_payload)
+ except DecryptionFailed as error:
+ log.warning(error)
+ return
+
+ signcrypt = Node(node=decrypted_payload)
+
+ signcrypt_jid = signcrypt.getTagAttr('to', 'jid')
+ if self.own_jid != signcrypt_jid:
+ log.warning('signcrypt "to" attr %s != %s',
+ self.own_jid, signcrypt_jid)
+ log.debug(signcrypt)
+ return
+
+ payload = signcrypt.getTag('payload')
+
+ body = None
+ if obj.name == 'message-received':
+ obj.stanza.delChild(enc_tag)
+ for node in payload.getChildren():
+ if node.name == 'body':
+ body = node.getData()
+ obj.stanza.setTagData('body', body)
+ else:
+ obj.stanza.addChild(node=node)
+ else:
+ obj.msg_.delChild(enc_tag)
+ for node in payload.getChildren():
+ if node.name == 'body':
+ body = node.getData()
+ obj.msg_.setTagData('body', node.getData())
+ else:
+ obj.msg_.addChild(node=node)
+
+ if body:
+ obj.msgtxt = body
+
+ obj.encrypted = ENCRYPTION_NAME
+ callback(obj)
+
+ def encrypt_message(self, obj, callback):
+ keys = self._contacts.get_keys(obj.jid)
+ if not keys:
+ # TODO: this should never happen in theory
+ log.error('Droping stanza to %s, because we have no key', obj.jid)
+ return
+
+ keys += self._contacts.get_keys(self.own_jid)
+ keys += [Key(self._fingerprint, None)]
+
+ payload = util.create_signcrypt_node(obj)
+
+ encrypted_payload, error = self._pgp.encrypt(payload, keys)
+ if error:
+ log.error('Error: %s', error)
+ app.nec.push_incoming_event(
+ MessageNotSentEvent(
+ None, conn=self._con, jid=obj.jid, message=obj.message,
+ error=error, time_=time.time()))
+ return
+
+ util.create_openpgp_message(obj, encrypted_payload)
+
+ obj.encrypted = ENCRYPTION_NAME
+ self.print_msg_to_log(obj.msg_iq)
+ callback(obj)
+
+ @staticmethod
+ def print_msg_to_log(stanza):
+ """ Prints a stanza in a fancy way to the log """
+ log.debug('-'*15)
+ stanzastr = '\n' + stanza.__str__(fancy=True)
+ stanzastr = stanzastr[0:-1]
+ log.debug(stanzastr)
+ log.debug('-'*15)
+
+ def get_keys(self, jid=None, only_trusted=True):
+ if jid is None:
+ jid = self.own_jid
+ return self._contacts.get_keys(jid, only_trusted=only_trusted)
+
+ def clear_fingerprints(self):
+ self._publish_key_list()
+
+ def cleanup(self):
+ self._storage.cleanup()
+ self._pgp = None
+ self._contacts = None
+
+
+def get_instance(*args, **kwargs):
+ return OpenPGP(*args, **kwargs), 'OpenPGP'
diff --git a/openpgp/modules/pgp_keylist.py b/openpgp/modules/pgp_keylist.py
new file mode 100644
index 0000000..97f0283
--- /dev/null
+++ b/openpgp/modules/pgp_keylist.py
@@ -0,0 +1,124 @@
+# Copyright (C) 2018 Philipp Hörist <philipp AT hoerist.com>
+#
+# This file is part of Gajim.
+#
+# Gajim 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; version 3 only.
+#
+# Gajim 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 Gajim. If not, see <http://www.gnu.org/licenses/>.
+
+# XEP-0373: OpenPGP for XMPP
+
+import logging
+import time
+
+import nbxmpp
+
+from gajim.common import app
+from gajim.common.exceptions import StanzaMalformed
+from gajim.common.modules.pep import AbstractPEPModule, AbstractPEPData
+from gajim.common.modules.date_and_time import parse_datetime
+
+from openpgp.modules import util
+from openpgp.modules.util import Key
+
+log = logging.getLogger('gajim.plugin_system.openpgp.pep')
+
+# Module name
+name = 'PGPKeylist'
+zeroconf = False
+
+
+class PGPKeylistData(AbstractPEPData):
+
+ type_ = 'openpgp-keylist'
+
+ def __init__(self, keylist):
+ self._pep_specific_data = keylist
+ self.data = keylist
+
+
+class PGPKeylist(AbstractPEPModule):
+ '''
+ <item>
+ <public-keys-list xmlns='urn:xmpp:openpgp:0'>
+ <pubkey-metadata
+ v4-fingerprint='1357B01865B2503C18453D208CAC2A9678548E35'
+ date='2018-03-01T15:26:12Z'
+ />
+ <pubkey-metadata
+ v4-fingerprint='67819B343B2AB70DED9320872C6464AF2A8E4C02'
+ date='1953-05-16T12:00:00Z'
+ />
+ </public-keys-list>
+ </item>
+ '''
+
+ name = 'openpgp-keylist'
+ namespace = util.NS_OPENPGP_PUBLIC_KEYS
+ pep_class = PGPKeylistData
+ store_publish = True
+ _log = log
+
+ def __init__(self, con):
+ AbstractPEPModule.__init__(self, con, con.name)
+
+ self.handlers = []
+
+ def _extract_info(self, item):
+ keylist_tag = item.getTag('public-keys-list',
+ namespace=util.NS_OPENPGP)
+ if keylist_tag is None:
+ raise StanzaMalformed('No public-keys-list node')
+
+ metadata = keylist_tag.getTags('pubkey-metadata')
+ if not metadata:
+ raise StanzaMalformed('No metadata found')
+
+ keylist = []
+ for data in metadata:
+ attrs = data.getAttrs()
+
+ if not attrs or 'v4-fingerprint' not in attrs:
+ raise StanzaMalformed('No fingerprint in metadata')
+
+ date = attrs.get('date', None)
+ if date is None:
+ raise StanzaMalformed('No date in metadata')
+ else:
+ timestamp = parse_datetime(date, epoch=True)
+ if timestamp is None:
+ raise StanzaMalformed('Invalid date timestamp: %s', date)
+
+ keylist.append(Key(attrs['v4-fingerprint'], timestamp))
+ return keylist
+
+ def _notification_received(self, jid, keylist):
+ con = app.connections[self._account]
+ con.get_module('OpenPGP').key_list_received(keylist.data,
+ jid.getStripped())
+
+ def _build_node(self, keylist):
+ keylist_node = nbxmpp.Node('public-keys-list',
+ {'xmlns': util.NS_OPENPGP})
+ if keylist is None:
+ return keylist_node
+ for key in keylist:
+ attrs = {'v4-fingerprint': key.fingerprint}
+ if key.date is not None:
+ date = time.strftime(
+ '%Y-%m-%dT%H:%M:%SZ', time.gmtime(key.date))
+ attrs['date'] = date
+ keylist_node.addChild('pubkey-metadata', attrs=attrs)
+ return keylist_node
+
+
+def get_instance(*args, **kwargs):
+ return PGPKeylist(*args, **kwargs), 'PGPKeylist'
diff --git a/openpgp/modules/util.py b/openpgp/modules/util.py
new file mode 100644
index 0000000..b835190
--- /dev/null
+++ b/openpgp/modules/util.py
@@ -0,0 +1,205 @@
+# Copyright (C) 2018 Philipp Hörist <philipp AT hoerist.com>
+#
+# This file is part of Gajim.
+#
+# Gajim 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; version 3 only.
+#
+# Gajim 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 Gajim. If not, see <http://www.gnu.org/licenses/>.
+
+# XEP-0373: OpenPGP for XMPP
+
+import logging
+import random
+import time
+import string
+from enum import IntEnum
+from collections import namedtuple
+from base64 import b64decode, b64encode
+
+import nbxmpp
+from nbxmpp import Node
+
+NS_OPENPGP = 'urn:xmpp:openpgp:0'
+NS_OPENPGP_PUBLIC_KEYS = 'urn:xmpp:openpgp:0:public-keys'
+NS_NOTIFY = NS_OPENPGP_PUBLIC_KEYS + '+notify'
+
+NOT_ENCRYPTED_TAGS = [('no-store', nbxmpp.NS_MSG_HINTS),
+ ('store', nbxmpp.NS_MSG_HINTS),
+ ('no-copy', nbxmpp.NS_MSG_HINTS),
+ ('no-permanent-store', nbxmpp.NS_MSG_HINTS),
+ ('thread', None)]
+
+Key = namedtuple('Key', 'fingerprint date')
+
+log = logging.getLogger('gajim.plugin_system.openpgp.util')
+
+
+class Trust(IntEnum):
+ NOT_TRUSTED = 0
+ UNKNOWN = 1
+ BLIND = 2
+ VERIFIED = 3
+
+
+def unpack_public_key_list(stanza, from_jid):
+ fingerprints = []
+
+ parent = stanza.getTag('pubsub', namespace=nbxmpp.NS_PUBSUB)
+ if parent is None:
+ parent = stanza.getTag('event', namespace=nbxmpp.NS_PUBSUB_EVENT)
+ if parent is None:
+ log.warning('PGP keys list has no pubsub/event node')
+ return
+
+ items = parent.getTag('items', attrs={'node': NS_OPENPGP_PUBLIC_KEYS})
+ if items is None:
+ log.warning('PGP keys list has no items node')
+ return
+
+ item = items.getTags('item')
+ if not item:
+ log.warning('PGP keys list has no item node')
+ return
+
+ if len(item) > 1:
+ log.warning('PGP keys list has more than one item')
+ return
+
+ key_list = item[0].getTag('public-keys-list', namespace=NS_OPENPGP)
+ if key_list is None:
+ log.warning('PGP keys list has no public-keys-list node')
+ return
+
+ metadata = key_list.getTags('pubkey-metadata')
+ if not metadata:
+ return []
+
+ for node in metadata:
+ attrs = node.getAttrs()
+ if 'v4-fingerprint' not in attrs:
+ log.warning('No fingerprint in metadata node')
+ return
+
+ date = attrs.get('date', None)
+
+ fingerprints.append(
+ Key(attrs['v4-fingerprint'], date))
+
+ return fingerprints
+
+
+def unpack_public_key(stanza, fingerprint):
+ pubsub = stanza.getTag('pubsub', namespace=nbxmpp.NS_PUBSUB)
+ if pubsub is None:
+ log.warning('PGP public key has no pubsub node')
+ return
+ node = '%s:%s' % (NS_OPENPGP_PUBLIC_KEYS, fingerprint)
+ items = pubsub.getTag('items', attrs={'node': node})
+ if items is None:
+ log.warning('PGP public key has no items node')
+ return
+
+ item = items.getTags('item')
+ if not item:
+ log.warning('PGP public key has no item node')
+ return
+
+ if len(item) > 1:
+ log.warning('PGP public key has more than one item')
+ return
+
+ pub_key = item[0].getTag('pubkey', namespace=NS_OPENPGP)
+ if pub_key is None:
+ log.warning('PGP public key has no pubkey node')
+ return
+
+ data = pub_key.getTag('data')
+ if data is None:
+ log.warning('PGP public key has no data node')
+ return
+
+ return b64decode(data.getData().encode('utf8'))
+
+
+def create_signcrypt_node(obj):
+ '''
+ <signcrypt xmlns='urn:xmpp:openpgp:0'>
+ <to jid='juliet@example.org'/>
+ <time stamp='2014-07-10T17:06:00+02:00'/>
+ <rpad>
+ f0rm1l4n4-mT8y33j!Y%fRSrcd^ZE4Q7VDt1L%WEgR!kv
+ </rpad>
+ <payload>
+ <body xmlns='jabber:client'>
+ This is a secret message.
+ </body>
+ </payload>
+ </signcrypt>
+ '''
+
+ encrypted_nodes = []
+ child_nodes = obj.msg_iq.getChildren()
+ for node in child_nodes:
+ if (node.name, node.namespace) not in NOT_ENCRYPTED_TAGS:
+ if not node.namespace:
+ node.setNamespace(nbxmpp.NS_CLIENT)
+ encrypted_nodes.append(node)
+ obj.msg_iq.delChild(node)
+
+ signcrypt = Node('signcrypt', attrs={'xmlns': NS_OPENPGP})
+ signcrypt.addChild('to', attrs={'jid': obj.jid})
+
+ timestamp = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
+ signcrypt.addChild('time', attrs={'stamp': timestamp})
+
+ signcrypt.addChild('rpad').addData(get_rpad())
+
+ payload = signcrypt.addChild('payload')
+
+ for node in encrypted_nodes:
+ payload.addChild(node=node)
+
+ return signcrypt
+
+
+def get_rpad():
+ rpad_range = random.randint(30, 50)
+ return ''.join(
+ random.choice(string.ascii_letters) for _ in range(rpad_range))
+
+
+def create_openpgp_message(obj, encrypted_payload):
+ b64encoded_payload = b64encode(
+ encrypted_payload.encode('utf-8')).decode('utf8')
+
+ openpgp_node = nbxmpp.Node('openpgp', attrs={'xmlns': NS_OPENPGP})
+ openpgp_node.addData(b64encoded_payload)
+ obj.msg_iq.addChild(node=openpgp_node)
+
+ eme_node = nbxmpp.Node('encryption',
+ attrs={'xmlns': nbxmpp.NS_EME,
+ 'namespace': NS_OPENPGP})
+ obj.msg_iq.addChild(node=eme_node)
+
+ if obj.message:
+ obj.msg_iq.setBody(get_info_message())
+
+
+def get_info_message():
+ return '[This message is *encrypted* with OpenPGP (See :XEP:`0373`]'
+
+
+class VerifyFailed(Exception):
+ pass
+
+
+class DecryptionFailed(Exception):
+ pass
diff --git a/openpgp/pgpplugin.py b/openpgp/pgpplugin.py
new file mode 100644
index 0000000..90884a0
--- /dev/null
+++ b/openpgp/pgpplugin.py
@@ -0,0 +1,194 @@
+# Copyright (C) 2018 Philipp Hörist <philipp AT hoerist.com>
+#
+# This file is part of Gajim.
+#
+# Gajim 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; version 3 only.
+#
+# Gajim 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 Gajim. If not, see <http://www.gnu.org/licenses/>.
+
+# XEP-0373: OpenPGP for XMPP
+
+import logging
+import os
+from pathlib import Path
+
+from gi.repository import Gtk
+from gi.repository import Gdk
+
+from gajim.plugins import GajimPlugin
+from gajim.common import app
+from gajim.common import ged
+from gajim.common import configpaths
+from gajim.common import helpers
+from gajim.common.const import CSSPriority
+from gajim.gtk.dialogs import ErrorDialog
+
+from openpgp.modules.util import NS_NOTIFY
+from openpgp.modules import pgp_keylist
+try:
+ from openpgp.modules import openpgp
+except ImportError as e:
+ ERROR_MSG = str(e)
+else:
+ ERROR_MSG = None
+
+log = logging.getLogger('gajim.plugin_system.openpgp')
+
+
+#TODO: we cant encrypt "thread" right now, because its needed for Gajim to find ChatControls.
+
+class OpenPGPPlugin(GajimPlugin):
+ def init(self):
+ if ERROR_MSG:
+ self.activatable = False
+ self.available_text = ERROR_MSG
+ self.config_dialog = None
+ return
+
+ self.events_handlers = {
+ 'signed-in': (ged.PRECORE, self.signed_in),
+ }
+
+ self.modules = [pgp_keylist,
+ openpgp]
+
+ self.encryption_name = 'OpenPGP'
+ self.config_dialog = None
+ self.gui_extension_points = {
+ 'encrypt' + self.encryption_name: (self._encrypt_message, None),
+ 'decrypt': (self._decrypt_message, None),
+ 'send_message' + self.encryption_name: (
+ self._before_sendmessage, None),
+ 'encryption_dialog' + self.encryption_name: (
+ self.on_encryption_button_clicked, None),
+ 'encryption_state' + self.encryption_name: (
+ self.encryption_state, None),
+ 'update_caps': (self._update_caps, None),
+ }
+
+ self.connections = {}
+
+ self.plugin = self
+ self.announced = []
+ self.own_key = None
+ self.pgp_instances = {}
+ self._create_paths()
+ self._load_css()
+
+ def _load_css(self):
+ path = Path(__file__).parent / 'gtk' / 'style.css'
+ try:
+ with open(path, "r") as f:
+ css = f.read()
+ except Exception as exc:
+ log.error('Error loading css: %s', exc)
+ return
+
+ try:
+ provider = Gtk.CssProvider()
+ provider.load_from_data(bytes(css.encode('utf-8')))
+ Gtk.StyleContext.add_provider_for_screen(Gdk.Screen.get_default(),
+ provider,
+ CSSPriority.DEFAULT_THEME)
+ except Exception:
+ log.exception('Error loading application css')
+
+ def _create_paths(self):
+ keyring_path = os.path.join(configpaths.get('MY_DATA'), 'openpgp')
+ if not os.path.exists(keyring_path):
+ os.makedirs(keyring_path)
+
+ def signed_in(self, event):
+ account = event.conn.name
+ con = app.connections[account]
+ if con.get_module('OpenPGP').secret_key_available:
+ log.info('%s => Publish keylist and public key after sign in',
+ account)
+ con.get_module('OpenPGP').query_key_list()
+ con.get_module('OpenPGP').publish_key()
+
+ def activate(self):
+ for account in app.connections:
+ if app.caps_hash[account] != '':
+ # Gajim has already a caps hash calculated, update it
+ helpers.update_optional_features(account)
+
+ con = app.connections[account]
+ if app.account_is_connected(account):
+ if con.get_module('OpenPGP').secret_key_available:
+ log.info('%s => Publish keylist and public key '
+ 'after plugin activation', account)
+ con.get_module('OpenPGP').query_key_list()
+ con.get_module('OpenPGP').publish_key()
+
+ def deactivate(self):
+ pass
+
+ @staticmethod
+ def _update_caps(account):
+ if NS_NOTIFY not in app.gajim_optional_features[account]:
+ app.gajim_optional_features[account].append(NS_NOTIFY)
+
+ def activate_encryption(self, chat_control):
+ account = chat_control.account
+ jid = chat_control.contact.jid
+ con = app.connections[account]
+ if con.get_module('OpenPGP').secret_key_available:
+ keys = app.connections[account].get_module('OpenPGP').get_keys(
+ jid, only_trusted=False)
+ if not keys:
+ ErrorDialog(
+ _('No OpenPGP key'),
+ _('We didnt receive a OpenPGP key from this contact.'))
+ return
+ return True
+ else:
+ from openpgp.gtk.wizard import KeyWizard
+ KeyWizard(self, account, chat_control)
+
+ def encryption_state(self, chat_control, state):
+ state['authenticated'] = True
+ state['visible'] = True
+
+ def on_encryption_button_clicked(self, chat_control):
+ account = chat_control.account
+ jid = chat_control.contact.jid
+ transient = chat_control.parent_win.window
+
+ from openpgp.gtk.key import KeyDialog
+ KeyDialog(account, jid, transient)
+
+ def _before_sendmessage(self, chat_control):
+ account = chat_control.account
+ jid = chat_control.contact.jid
+ con = app.connections[account]
+
+ if not con.get_module('OpenPGP').secret_key_available:
+ from openpgp.gtk.wizard import KeyWizard
+ KeyWizard(self, account, chat_control)
+ return
+
+ keys = con.get_module('OpenPGP').get_keys(jid)
+ if not keys:
+ ErrorDialog(
+ _('Not Trusted'),
+ _('There was no trusted and active key found'))
+ chat_control.sendmessage = False
+
+ def _encrypt_message(self, con, obj, callback):
+ if not con.get_module('OpenPGP').secret_key_available:
+ return
+ con.get_module('OpenPGP').encrypt_message(obj, callback)
+
+ def _decrypt_message(self, con, obj, callback):
+ if not con.get_module('OpenPGP').secret_key_available:
+ return
+ con.get_module('OpenPGP').decrypt_message(obj, callback)