[openpgp] Inital commit

This commit is contained in:
Philipp Hörist
2017-04-11 23:49:27 +02:00
parent bd1b120b80
commit 7776ae0a59
15 changed files with 2084 additions and 0 deletions

1
openpgp/__init__.py Normal file
View File

@@ -0,0 +1 @@
from .pgpplugin import OpenPGPPlugin

View File

196
openpgp/backend/gpgme.py Normal file
View File

@@ -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')

183
openpgp/backend/pygpg.py Normal file
View File

@@ -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)

102
openpgp/backend/sql.py Normal file
View File

@@ -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()

0
openpgp/gtk/__init__.py Normal file
View File

263
openpgp/gtk/key.py Normal file
View File

@@ -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)

17
openpgp/gtk/style.css Normal file
View File

@@ -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; }

253
openpgp/gtk/wizard.py Normal file
View File

@@ -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)

8
openpgp/manifest.ini Normal file
View File

@@ -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

View File

538
openpgp/modules/openpgp.py Normal file
View File

@@ -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'

View File

@@ -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'

205
openpgp/modules/util.py Normal file
View File

@@ -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

194
openpgp/pgpplugin.py Normal file
View File

@@ -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)