pgp/0000755000175500017550000000000013772223274011407 5ustar debacledebaclepgp/gtk/0000755000175500017550000000000013772223274012174 5ustar debacledebaclepgp/gtk/key.py0000644000175500017550000001213213772223274013335 0ustar debacledebacle# Copyright (C) 2019 Philipp Hörist # # This file is part of the PGP Gajim Plugin. # # PGP Gajim Plugin 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. # # PGP Gajim Plugin 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 PGP Gajim Plugin. If not, see . from pathlib import Path from gi.repository import Gtk from gi.repository import GLib from gajim.common import app from gajim.plugins.plugins_i18n import _ from gajim.plugins.helpers import get_builder class KeyDialog(Gtk.Dialog): def __init__(self, plugin, account, jid, transient): super().__init__(title=_('Assign key for %s') % jid, destroy_with_parent=True) self.set_transient_for(transient) self.set_resizable(True) self.set_default_size(450, -1) self._plugin = plugin self._jid = jid self._con = app.connections[account] self._label = Gtk.Label() self._assign_button = Gtk.Button(label=_('Assign Key')) self._assign_button.get_style_context().add_class('suggested-action') self._assign_button.set_halign(Gtk.Align.CENTER) self._assign_button.set_margin_top(18) self._assign_button.connect('clicked', self._choose_key) box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) box.set_border_width(18) box.add(self._label) box.add(self._assign_button) area = self.get_content_area() area.pack_start(box, True, True, 0) self._load_key() self.show_all() def _choose_key(self, *args): backend = self._con.get_module('PGPLegacy').pgp_backend dialog = ChooseGPGKeyDialog(backend.get_keys(), self) dialog.connect('response', self._on_response) def _load_key(self): key_data = self._con.get_module('PGPLegacy').get_contact_key_data( self._jid) if key_data is None: self._set_key(None) else: self._set_key(key_data.values()) def _on_response(self, dialog, response): if response != Gtk.ResponseType.OK: return if dialog.selected_key is None: self._con.get_module('PGPLegacy').set_contact_key_data( self._jid, None) self._set_key(None) else: self._con.get_module('PGPLegacy').set_contact_key_data( self._jid, dialog.selected_key) self._set_key(dialog.selected_key) def _set_key(self, key_data): if key_data is None: self._label.set_text(_('No key assigned')) else: key_id, key_user = key_data self._label.set_markup('%s %s' % \ (key_id, GLib.markup_escape_text(key_user))) class ChooseGPGKeyDialog(Gtk.Dialog): def __init__(self, secret_keys, transient_for): Gtk.Dialog.__init__(self, title=_('Assign PGP Key'), transient_for=transient_for) secret_keys[_('None')] = _('None') self.set_position(Gtk.WindowPosition.CENTER_ON_PARENT) self.set_resizable(True) self.set_default_size(500, 300) self.add_button(_('Cancel'), Gtk.ResponseType.CANCEL) self.add_button(_('OK'), Gtk.ResponseType.OK) self._selected_key = None ui_path = Path(__file__).parent self._ui = get_builder(ui_path.resolve() / 'choose_key.ui') self._ui.keys_treeview = self._ui.keys_treeview model = self._ui.keys_treeview.get_model() model.set_sort_func(1, self._sort) model = self._ui.keys_treeview.get_model() for key_id in secret_keys.keys(): model.append((key_id, secret_keys[key_id])) self.get_content_area().add(self._ui.box) self._ui.connect_signals(self) self.connect_after('response', self._on_response) self.show_all() @property def selected_key(self): return self._selected_key @staticmethod def _sort(model, iter1, iter2, _data): value1 = model[iter1][1] value2 = model[iter2][1] if value1 == _('None'): return -1 if value2 == _('None'): return 1 if value1 < value2: return -1 return 1 def _on_response(self, _dialog, _response): self.destroy() def _on_row_changed(self, treeview): selection = treeview.get_selection() model, iter_ = selection.get_selected() if iter_ is None: self._selected_key = None else: key_id, key_user = model[iter_][0], model[iter_][1] if key_id == _('None'): self._selected_key = None else: self._selected_key = key_id, key_user pgp/gtk/config.py0000644000175500017550000000724613772223274014024 0ustar debacledebacle# Copyright (C) 2019 Philipp Hörist # # This file is part of the PGP Gajim Plugin. # # PGP Gajim Plugin 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. # # PGP Gajim Plugin 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 PGP Gajim Plugin. If not, see . from pathlib import Path from gi.repository import Gtk from gi.repository import Gdk from gi.repository import GLib from gajim.common import app from gajim.plugins.helpers import get_builder from gajim.plugins.plugins_i18n import _ from pgp.gtk.key import ChooseGPGKeyDialog class PGPConfigDialog(Gtk.ApplicationWindow): def __init__(self, plugin, parent): Gtk.ApplicationWindow.__init__(self) self.set_application(app.app) self.set_show_menubar(False) self.set_title(_('PGP Configuration')) self.set_transient_for(parent) self.set_resizable(True) self.set_type_hint(Gdk.WindowTypeHint.DIALOG) self.set_destroy_with_parent(True) ui_path = Path(__file__).parent self._ui = get_builder(ui_path.resolve() / 'config.ui') self.add(self._ui.config_box) self._ui.connect_signals(self) self._plugin = plugin for account in app.connections.keys(): page = Page(plugin, account) self._ui.stack.add_titled(page, account, app.get_account_label(account)) self.show_all() class Page(Gtk.Box): def __init__(self, plugin, account): Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL) self._con = app.connections[account] self._plugin = plugin self._label = Gtk.Label() self._button = Gtk.Button(label=_('Assign Key')) self._button.get_style_context().add_class('suggested-action') self._button.set_halign(Gtk.Align.CENTER) self._button.set_margin_top(18) self._button.connect('clicked', self._on_assign) self._load_key() self.add(self._label) self.add(self._button) self.show_all() def _on_assign(self, _button): backend = self._con.get_module('PGPLegacy').pgp_backend secret_keys = backend.get_keys(secret=True) dialog = ChooseGPGKeyDialog(secret_keys, self.get_toplevel()) dialog.connect('response', self._on_response) def _load_key(self): key_data = self._con.get_module('PGPLegacy').get_own_key_data() if key_data is None: self._set_key(None) else: self._set_key((key_data['key_id'], key_data['key_user'])) def _on_response(self, dialog, response): if response != Gtk.ResponseType.OK: return if dialog.selected_key is None: self._con.get_module('PGPLegacy').set_own_key_data(None) self._set_key(None) else: self._con.get_module('PGPLegacy').set_own_key_data( dialog.selected_key) self._set_key(dialog.selected_key) def _set_key(self, key_data): if key_data is None: self._label.set_text(_('No key assigned')) else: key_id, key_user = key_data self._label.set_markup('%s %s' % \ (key_id, GLib.markup_escape_text(key_user))) pgp/gtk/choose_key.ui0000644000175500017550000000521713772223274014670 0ustar debacledebacle 500 300 True False 18 vertical 6 True True True in True True liststore 1 Key ID descending 0 Contact Name 1 1 True True 0 pgp/gtk/__init__.py0000644000175500017550000000000013772223274014273 0ustar debacledebaclepgp/gtk/config.ui0000644000175500017550000000263613772223274014007 0ustar debacledebacle True False 12 True False stack False True 0 400 350 True False True 18 crossfade False True 1 pgp/modules/0000755000175500017550000000000013772223274013057 5ustar debacledebaclepgp/modules/util.py0000644000175500017550000000266213772223274014414 0ustar debacledebacle# Copyright (C) 2019 Philipp Hörist # # This file is part of the PGP Gajim Plugin. # # PGP Gajim Plugin 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. # # PGP Gajim Plugin 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 PGP Gajim Plugin. If not, see . import os import subprocess from nbxmpp.namespaces import Namespace def prepare_stanza(stanza, plaintext): delete_nodes(stanza, 'encrypted', Namespace.ENCRYPTED) delete_nodes(stanza, 'body') stanza.setBody(plaintext) def delete_nodes(stanza, name, namespace=None): nodes = stanza.getTags(name, namespace=namespace) for node in nodes: stanza.delChild(node) def find_gpg(): def _search(binary): if os.name == 'nt': gpg_cmd = binary + ' -h >nul 2>&1' else: gpg_cmd = binary + ' -h >/dev/null 2>&1' if subprocess.call(gpg_cmd, shell=True): return False return True if _search('gpg2'): return 'gpg2' if _search('gpg'): return 'gpg' pgp/modules/pgp_legacy.py0000644000175500017550000002505013772223274015545 0ustar debacledebacle# Copyright (C) 2019 Philipp Hörist # # This file is part of the PGP Gajim Plugin. # # PGP Gajim Plugin 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. # # PGP Gajim Plugin 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 PGP Gajim Plugin. If not, see . import os import time import threading import nbxmpp from nbxmpp.namespaces import Namespace from nbxmpp.structs import StanzaHandler from gi.repository import GLib from gajim.common import app from gajim.common.nec import NetworkEvent from gajim.common.const import EncryptionData from gajim.common.modules.base import BaseModule from gajim.plugins.plugins_i18n import _ from pgp.backend.python_gnupg import PGP from pgp.modules.util import prepare_stanza from pgp.backend.store import KeyStore from pgp.exceptions import SignError from pgp.exceptions import KeyMismatch from pgp.exceptions import NoKeyIdFound # Module name name = 'PGPLegacy' zeroconf = True ALLOWED_TAGS = [('request', Namespace.RECEIPTS), ('active', Namespace.CHATSTATES), ('gone', Namespace.CHATSTATES), ('inactive', Namespace.CHATSTATES), ('paused', Namespace.CHATSTATES), ('composing', Namespace.CHATSTATES), ('no-store', Namespace.HINTS), ('store', Namespace.HINTS), ('no-copy', Namespace.HINTS), ('no-permanent-store', Namespace.HINTS), ('replace', Namespace.CORRECT), ('origin-id', Namespace.SID), ] class PGPLegacy(BaseModule): def __init__(self, con): BaseModule.__init__(self, con, plugin=True) self.handlers = [ StanzaHandler(name='message', callback=self._message_received, ns=Namespace.ENCRYPTED, priority=9), StanzaHandler(name='presence', callback=self._on_presence_received, ns=Namespace.SIGNED, priority=48), ] self.own_jid = self._con.get_own_jid() self._pgp = PGP() self._store = KeyStore(self._account, self.own_jid, self._log, self._pgp.list_keys) self._always_trust = [] self._presence_fingerprint_store = {} @property def pgp_backend(self): return self._pgp def set_own_key_data(self, *args, **kwargs): return self._store.set_own_key_data(*args, **kwargs) def get_own_key_data(self, *args, **kwargs): return self._store.get_own_key_data(*args, **kwargs) def set_contact_key_data(self, *args, **kwargs): return self._store.set_contact_key_data(*args, **kwargs) def get_contact_key_data(self, *args, **kwargs): return self._store.get_contact_key_data(*args, **kwargs) def has_valid_key_assigned(self, jid): key_data = self.get_contact_key_data(jid) if key_data is None: return False key_id = key_data['key_id'] announced_fingerprint = self._presence_fingerprint_store.get(jid) if announced_fingerprint is None: return True if announced_fingerprint == key_id: return True raise KeyMismatch(announced_fingerprint) def _on_presence_received(self, _con, _stanza, properties): if properties.signed is None: return jid = properties.jid.getBare() fingerprint = self._pgp.verify(properties.status, properties.signed) if fingerprint is None: self._log.info('Presence from %s was signed but no corresponding ' 'key was found', jid) return self._presence_fingerprint_store[jid] = fingerprint self._log.info('Presence from %s was verified successfully, ' 'fingerprint: %s', jid, fingerprint) key_data = self.get_contact_key_data(jid) if key_data is None: self._log.info('No key assigned for contact: %s', jid) return if key_data['key_id'] != fingerprint: self._log.warning('Fingerprint mismatch, ' 'Presence was signed with fingerprint: %s, ' 'Assigned key fingerprint: %s', fingerprint, key_data['key_id']) return def _message_received(self, _con, stanza, properties): if not properties.is_pgp_legacy or properties.from_muc: return from_jid = properties.jid.getBare() self._log.info('Message received from: %s', from_jid) payload = self._pgp.decrypt(properties.pgp_legacy) prepare_stanza(stanza, payload) properties.encrypted = EncryptionData({'name': 'PGP'}) def encrypt_message(self, con, event, callback): if not event.message: callback(event) return to_jid = app.get_jid_without_resource(event.jid) try: key_id, own_key_id = self._get_key_ids(to_jid) except NoKeyIdFound as error: self._log.warning(error) return always_trust = key_id in self._always_trust self._encrypt(con, event, [key_id, own_key_id], callback, always_trust) def _encrypt(self, con, event, keys, callback, always_trust): result = self._pgp.encrypt(event.message, keys, always_trust) encrypted_payload, error = result if error: self._handle_encrypt_error(con, error, event, keys, callback) return self._cleanup_stanza(event) self._create_pgp_legacy_message(event.stanza, encrypted_payload) event.xhtml = None event.encrypted = 'PGP' event.additional_data['encrypted'] = {'name': 'PGP'} callback(event) def _handle_encrypt_error(self, con, error, event, keys, callback): if error.startswith('NOT_TRUSTED'): def on_yes(checked): if checked: self._always_trust.append(keys[0]) self._encrypt(con, event, keys, callback, True) def on_no(): self._raise_message_not_sent(con, event, error) app.nec.push_incoming_event( NetworkEvent('pgp-not-trusted', on_yes=on_yes, on_no=on_no)) else: self._raise_message_not_sent(con, event, error) @staticmethod def _raise_message_not_sent(con, event, error): session = event.session if hasattr(event, 'session') else None app.nec.push_incoming_event( NetworkEvent('message-not-sent', conn=con, jid=event.jid, message=event.message, error=_('Encryption error: %s') % error, time_=time.time(), session=session)) def _create_pgp_legacy_message(self, stanza, payload): stanza.setBody(self._get_info_message()) stanza.setTag('x', namespace=Namespace.ENCRYPTED).setData(payload) eme_node = nbxmpp.Node('encryption', attrs={'xmlns': Namespace.EME, 'namespace': Namespace.ENCRYPTED}) stanza.addChild(node=eme_node) def sign_presence(self, presence, status): key_data = self.get_own_key_data() if key_data is None: self._log.warning('No own key id found, cant sign presence') return try: result = self._pgp.sign(status, key_data['key_id']) except SignError as error: self._log.warning('Sign Error: %s', error) return # self._log.debug(self._pgp.sign.cache_info()) self._log.info('Presence signed') presence.setTag(Namespace.SIGNED + ' x').setData(result) @staticmethod def _get_info_message(): msg = '[This message is *encrypted* (See :XEP:`27`]' lang = os.getenv('LANG') if lang is not None and not lang.startswith('en'): # we're not english: one in locale and one en msg = _('[This message is *encrypted* (See :XEP:`27`]') + \ ' (' + msg + ')' return msg def _get_key_ids(self, jid): key_data = self.get_contact_key_data(jid) if key_data is None: raise NoKeyIdFound('No key id found for %s' % jid) key_id = key_data['key_id'] own_key_data = self.get_own_key_data() if own_key_data is None: raise NoKeyIdFound('Own key id not found') own_key_id = own_key_data['key_id'] return key_id, own_key_id @staticmethod def _cleanup_stanza(obj): ''' We make sure only allowed tags are in the stanza ''' stanza = nbxmpp.Message( to=obj.stanza.getTo(), typ=obj.stanza.getType()) stanza.setID(obj.stanza.getID()) stanza.setThread(obj.stanza.getThread()) for tag, ns in ALLOWED_TAGS: node = obj.stanza.getTag(tag, namespace=ns) if node: stanza.addChild(node=node) obj.stanza = stanza def encrypt_file(self, file, callback): thread = threading.Thread(target=self._encrypt_file_thread, args=(file, callback)) thread.daemon = True thread.start() def _encrypt_file_thread(self, file, callback): try: key_id, own_key_id = self._get_key_ids(file.contact.jid) except NoKeyIdFound as error: self._log.warning(error) return encrypted = self._pgp.encrypt_file(file.get_data(), [key_id, own_key_id]) if not encrypted: GLib.idle_add(self._on_file_encryption_error, encrypted.status) return file.size = len(encrypted.data) file.set_uri_transform_func(lambda uri: '%s.pgp' % uri) file.set_encrypted_data(encrypted.data) GLib.idle_add(callback, file) @staticmethod def _on_file_encryption_error(error): app.nec.push_incoming_event( NetworkEvent('pgp-file-encryption-error', error=error)) def get_instance(*args, **kwargs): return PGPLegacy(*args, **kwargs), 'PGPLegacy' pgp/modules/__init__.py0000644000175500017550000000000013772223274015156 0ustar debacledebaclepgp/pgp.png0000644000175500017550000004300613772223274012706 0ustar debacledebaclePNG  IHDRa pHYs+BiTXtXML:com.adobe.xmp Adobe Photoshop CC 2015 (Windows) 2018-04-11T09:37:37+02:00 2018-04-11T09:54:14+02:00 2018-04-11T09:54:14+02:00 image/png 3 adobe:docid:photoshop:356b697c-3d5c-11e8-a5a8-9b3fdeeac916 xmp.iid:dcfea758-14e6-c64b-95c0-f37bfbc9e1fe adobe:docid:photoshop:7b04ee80-3d5d-11e8-a5a8-9b3fdeeac916 xmp.did:4e8c8c3c-f3af-294d-ad50-65ad2ed1ece1 created xmp.iid:4e8c8c3c-f3af-294d-ad50-65ad2ed1ece1 2018-04-11T09:37:37+02:00 Adobe Photoshop CC 2015 (Windows) converted from image/png to application/vnd.adobe.photoshop saved xmp.iid:7a8631e4-a0c7-a049-87d2-838e2b35ccdd 2018-04-11T09:54:07+02:00 Adobe Photoshop CC 2015 (Windows) / saved xmp.iid:801e915c-81cb-1740-9cf0-2d91b5216cdd 2018-04-11T09:54:14+02:00 Adobe Photoshop CC 2015 (Windows) / converted from application/vnd.adobe.photoshop to image/png derived converted from application/vnd.adobe.photoshop to image/png saved xmp.iid:dcfea758-14e6-c64b-95c0-f37bfbc9e1fe 2018-04-11T09:54:14+02:00 Adobe Photoshop CC 2015 (Windows) / xmp.iid:801e915c-81cb-1740-9cf0-2d91b5216cdd xmp.did:4e8c8c3c-f3af-294d-ad50-65ad2ed1ece1 xmp.did:4e8c8c3c-f3af-294d-ad50-65ad2ed1ece1 1 960000/10000 960000/10000 2 65535 16 16 ekPq`qi rfᝥ ZkR Umt${zߛ3WyI͜u`sCvEiċHZfN)u'/8ιfvvnAR}ʹeIENDB`pgp/exceptions.py0000644000175500017550000000146013772223274014143 0ustar debacledebacle# Copyright (C) 2019 Philipp Hörist # # This file is part of the PGP Gajim Plugin. # # PGP Gajim Plugin 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. # # PGP Gajim Plugin 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 PGP Gajim Plugin. If not, see . class SignError(Exception): pass class KeyMismatch(Exception): pass class NoKeyIdFound(Exception): pass pgp/backend/0000755000175500017550000000000013772223274012776 5ustar debacledebaclepgp/backend/store.py0000644000175500017550000001452713772223274014515 0ustar debacledebacle# Copyright (C) 2019 Philipp Hörist # # This file is part of the PGP Gajim Plugin. # # PGP Gajim Plugin 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. # # PGP Gajim Plugin 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 PGP Gajim Plugin. If not, see . import json from pathlib import Path from gajim.common import app from gajim.common import configpaths CURRENT_STORE_VERSION = 3 class KeyResolveError(Exception): pass class KeyStore: def __init__(self, account, own_jid, log, list_keys_func): self._list_keys_func = list_keys_func self._log = log self._account = account own_bare_jid = own_jid.getBare() path = Path(configpaths.get('PLUGINS_DATA')) / 'pgplegacy' / own_bare_jid if not path.exists(): path.mkdir(parents=True) self._store_path = path / 'store' if self._store_path.exists(): # having store v2 or higher with self._store_path.open('r') as file: try: self._store = json.load(file) except Exception: log.exception('Could not load config') self._store = self._empty_store() ver = self._store.get('_version', 2) if ver > CURRENT_STORE_VERSION: raise Exception('Unknown store version! ' 'Please upgrade pgp plugin.') elif ver == 2: self._migrate_v2_store() self._save_store() elif ver != CURRENT_STORE_VERSION: # garbled version self._store = self._empty_store() log.warning('Bad pgp key store version. Initializing new.') else: # having store v1 or fresh install self._store = self._empty_store() self._migrate_v1_store() self._migrate_v2_store() self._save_store() @staticmethod def _empty_store(): return { '_version': CURRENT_STORE_VERSION, 'own_key_data': None, 'contact_key_data': {}, } def _migrate_v1_store(self): keys = {} attached_keys = app.settings.get_account_setting( self._account, 'attached_gpg_keys') if not attached_keys: return attached_keys = attached_keys.split() for i in range(len(attached_keys) // 2): keys[attached_keys[2 * i]] = attached_keys[2 * i + 1] for jid, key_id in keys.items(): self._set_contact_key_data_nosync(jid, (key_id, '')) own_key_id = app.settings.get_account_setting(self._account, 'keyid') own_key_user = app.settings.get_account_setting( self._account, 'keyname') if own_key_id: self._set_own_key_data_nosync((own_key_id, own_key_user)) attached_keys = app.settings.set_account_setting( self._account, 'attached_gpg_keys', '') self._log.info('Migration from store v1 was successful') def _migrate_v2_store(self): own_key_data = self.get_own_key_data() if own_key_data is not None: own_key_id, own_key_user = (own_key_data['key_id'], own_key_data['key_user']) try: own_key_fp = self._resolve_short_id(own_key_id, has_secret=True) self._set_own_key_data_nosync((own_key_fp, own_key_user)) except KeyResolveError: self._set_own_key_data_nosync(None) prune_list = [] for dict_key, key_data in self._store['contact_key_data'].items(): try: key_data['key_id'] = self._resolve_short_id(key_data['key_id']) except KeyResolveError: prune_list.append(dict_key) for dict_key in prune_list: del self._store['contact_key_data'][dict_key] self._store['_version'] = CURRENT_STORE_VERSION self._log.info('Migration from store v2 was successful') def _save_store(self): with self._store_path.open('w') as file: json.dump(self._store, file) def _get_dict_key(self, jid): return '%s-%s' % (self._account, jid) def _resolve_short_id(self, short_id, has_secret=False): candidates = self._list_keys_func( secret=has_secret, keys=(short_id,)).fingerprints if len(candidates) == 1: return candidates[0] elif len(candidates) > 1: self._log.critical('Key collision during migration. ' 'Key ID is %s. Removing binding...', repr(short_id)) else: self._log.warning('Key %s was not found during migration. ' 'Removing binding...', repr(short_id)) raise KeyResolveError def set_own_key_data(self, key_data): self._set_own_key_data_nosync(key_data) self._save_store() def _set_own_key_data_nosync(self, key_data): if key_data is None: self._store['own_key_data'] = None else: self._store['own_key_data'] = { 'key_id': key_data[0], 'key_user': key_data[1] } def get_own_key_data(self): return self._store['own_key_data'] def get_contact_key_data(self, jid): key_ids = self._store['contact_key_data'] dict_key = self._get_dict_key(jid) return key_ids.get(dict_key) def set_contact_key_data(self, jid, key_data): self._set_contact_key_data_nosync(jid, key_data) self._save_store() def _set_contact_key_data_nosync(self, jid, key_data): key_ids = self._store['contact_key_data'] dict_key = self._get_dict_key(jid) if key_data is None: key_ids[dict_key] = None else: key_ids[dict_key] = { 'key_id': key_data[0], 'key_user': key_data[1] } pgp/backend/python_gnupg.py0000644000175500017550000001160713772223274016076 0ustar debacledebacle# Copyright (C) 2019 Philipp Hörist # Copyright (C) 2003-2014 Yann Leboulanger # Copyright (C) 2005 Alex Mauer # Copyright (C) 2005-2006 Nikos Kouremenos # Copyright (C) 2007 Stephan Erb # Copyright (C) 2008 Jean-Marie Traissard # Jonathan Schleifer # # This file is part of PGP Gajim Plugin. # # PGP Gajim Plugin 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. # # PGP Gajim Plugin 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 PGP Gajim Plugin. If not, see . import os import logging from functools import lru_cache import gnupg from gajim.common.helpers import Singleton from pgp.exceptions import SignError logger = logging.getLogger('gajim.p.pgplegacy') if logger.getEffectiveLevel() == logging.DEBUG: logger = logging.getLogger('gnupg') logger.addHandler(logging.StreamHandler()) logger.setLevel(logging.DEBUG) class PGP(gnupg.GPG, metaclass=Singleton): def __init__(self, binary, encoding=None): super().__init__(gpgbinary=binary, use_agent=True) if encoding is not None: self.encoding = encoding self.decode_errors = 'replace' def encrypt(self, payload, recipients, always_trust=False): if not always_trust: # check that we'll be able to encrypt result = self.get_key(recipients[0]) for key in result: if key['trust'] not in ('f', 'u'): return '', 'NOT_TRUSTED ' + key['keyid'][-8:] result = super().encrypt( payload.encode('utf8'), recipients, always_trust=always_trust) if result.ok: error = '' else: error = result.status return self._strip_header_footer(str(result)), error def decrypt(self, payload): data = self._add_header_footer(payload, 'MESSAGE') result = super().decrypt(data.encode('utf8')) return result.data.decode('utf8') @lru_cache(maxsize=8) def sign(self, payload, key_id): if payload is None: payload = '' result = super().sign(payload.encode('utf8'), keyid=key_id, detach=True) if result.fingerprint: return self._strip_header_footer(str(result)) raise SignError(result.status) def verify(self, payload, signed): # Hash algorithm is not transfered in the signed # presence stanza so try all algorithms. # Text name for hash algorithms from RFC 4880 - section 9.4 if payload is None: payload = '' hash_algorithms = ['SHA512', 'SHA384', 'SHA256', 'SHA224', 'SHA1', 'RIPEMD160'] for algo in hash_algorithms: data = os.linesep.join( ['-----BEGIN PGP SIGNED MESSAGE-----', 'Hash: ' + algo, '', payload, self._add_header_footer(signed, 'SIGNATURE')] ) result = super().verify(data.encode('utf8')) if result.valid: return result.fingerprint def get_key(self, key_id): return super().list_keys(keys=[key_id]) def get_keys(self, secret=False): keys = {} result = super().list_keys(secret=secret) for key in result: # Take first not empty uid keys[key['fingerprint']] = next(uid for uid in key['uids'] if uid) return keys @staticmethod def _strip_header_footer(data): """ Remove header and footer from data """ if not data: return '' lines = data.splitlines() while lines[0] != '': lines.remove(lines[0]) while lines[0] == '': lines.remove(lines[0]) i = 0 for line in lines: if line: if line[0] == '-': break i = i+1 line = '\n'.join(lines[0:i]) return line @staticmethod def _add_header_footer(data, type_): """ Add header and footer from data """ out = "-----BEGIN PGP %s-----" % type_ + os.linesep out = out + "Version: PGP" + os.linesep out = out + os.linesep out = out + data + os.linesep out = out + "-----END PGP %s-----" % type_ + os.linesep return out pgp/backend/__init__.py0000644000175500017550000000000013772223274015075 0ustar debacledebaclepgp/org.gajim.Gajim.Plugin.pgp.metainfo.xml0000644000175500017550000000074113772223274020661 0ustar debacledebacle org.gajim.Gajim.Plugin.pgp org.gajim.Gajim PGP Plugin XMPP Extension Protocol (XEP) for secure multi-client end-to-end encryption https://gajim.org/ CC-BY-SA-3.0 GPL-3.0 gajim-devel_AT_gajim.org pgp/__init__.py0000644000175500017550000000003613772223274013517 0ustar debacledebaclefrom .plugin import PGPPlugin pgp/manifest.ini0000644000175500017550000000040413772223274013714 0ustar debacledebacle[info] name: PGP short_name: pgp version: 1.3.5 description: PGP encryption as per XEP-0027. authors: Philipp Hörist homepage: https://dev.gajim.org/gajim/gajim-plugins/wikis/pgpplugin min_gajim_version: 1.2.91 max_gajim_version: 1.3.90 pgp/plugin.py0000644000175500017550000001440213772223274013260 0ustar debacledebacle# Copyright (C) 2019 Philipp Hörist # # This file is part of the PGP Gajim Plugin. # # PGP Gajim Plugin 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. # # PGP Gajim Plugin 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 PGP Gajim Plugin. If not, see . import os import logging from functools import partial from packaging.version import Version as V import nbxmpp from gajim.common import app from gajim.common import ged from gajim.plugins import GajimPlugin from gajim.plugins.plugins_i18n import _ from gajim.gui.dialogs import ErrorDialog from gajim.gui.dialogs import DialogButton from gajim.gui.dialogs import ConfirmationCheckDialog from pgp.gtk.key import KeyDialog from pgp.gtk.config import PGPConfigDialog from pgp.exceptions import KeyMismatch from pgp.modules.util import find_gpg ENCRYPTION_NAME = 'PGP' log = logging.getLogger('gajim.p.pgplegacy') ERROR = False try: import gnupg except ImportError: ERROR = True else: # We need https://pypi.python.org/pypi/python-gnupg # but https://pypi.python.org/pypi/gnupg shares the same package name. # It cannot be used as a drop-in replacement. # We test with a version check if python-gnupg is installed as it is # on a much lower version number than gnupg # Also we need at least python-gnupg 0.3.8 v_gnupg = gnupg.__version__ if V(v_gnupg) < V('0.3.8') or V(v_gnupg) > V('1.0.0'): log.error('We need python-gnupg >= 0.3.8') ERROR = True ERROR_MSG = None BINARY = find_gpg() log.info('Found GPG executable: %s', BINARY) if BINARY is None or ERROR: if os.name == 'nt': ERROR_MSG = _('Please install GnuPG / Gpg4win') else: ERROR_MSG = _('Please install python-gnupg and gnupg') else: from pgp.modules import pgp_legacy from pgp.backend.python_gnupg import PGP class PGPPlugin(GajimPlugin): def init(self): # pylint: disable=attribute-defined-outside-init self.description = _('PGP encryption as per XEP-0027') if ERROR_MSG: self.activatable = False self.config_dialog = None self.available_text = ERROR_MSG return self.config_dialog = partial(PGPConfigDialog, self) self.encryption_name = ENCRYPTION_NAME self.allow_zeroconf = True self.gui_extension_points = { 'encrypt' + ENCRYPTION_NAME: (self._encrypt_message, None), 'send_message' + ENCRYPTION_NAME: ( self._before_sendmessage, None), 'encryption_dialog' + ENCRYPTION_NAME: ( self._on_encryption_dialog, None), 'encryption_state' + ENCRYPTION_NAME: ( self._encryption_state, None), 'send-presence': (self._on_send_presence, None), } self.modules = [pgp_legacy] self.events_handlers = { 'pgp-not-trusted': (ged.PRECORE, self._on_not_trusted), 'pgp-file-encryption-error': (ged.PRECORE, self._on_file_encryption_error), } self._pgp = PGP(BINARY) @staticmethod def get_pgp_module(account): return app.connections[account].get_module('PGPLegacy') def activate(self): pass def deactivate(self): pass @staticmethod def activate_encryption(_chat_control): return True @staticmethod def _encryption_state(_chat_control, state): state['visible'] = True state['authenticated'] = True def _on_encryption_dialog(self, chat_control): account = chat_control.account jid = chat_control.contact.jid transient = chat_control.parent_win.window KeyDialog(self, account, jid, transient) def _on_send_presence(self, account, presence): status = presence.getStatus() self.get_pgp_module(account).sign_presence(presence, status) @staticmethod def _on_not_trusted(event): ConfirmationCheckDialog( _('Untrusted PGP key'), _('Untrusted PGP key'), _('The PGP key used to encrypt this chat is not ' 'trusted. Do you really want to encrypt this ' 'message?'), _('_Do not ask me again'), [DialogButton.make('Cancel', text=_('_No'), callback=event.on_no), DialogButton.make('OK', text=_('_Encrypt Anyway'), callback=event.on_yes)]).show() @staticmethod def _before_sendmessage(chat_control): account = chat_control.account jid = chat_control.contact.jid con = app.connections[account] try: valid = con.get_module('PGPLegacy').has_valid_key_assigned(jid) except KeyMismatch as announced_key_id: ErrorDialog( _('PGP Key mismatch'), _('The contact\'s key (%s) does not match the key ' 'assigned in Gajim.') % announced_key_id) chat_control.sendmessage = False return if not valid: ErrorDialog( _('No OpenPGP key assigned'), _('No OpenPGP key is assigned to this contact.')) chat_control.sendmessage = False elif con.get_module('PGPLegacy').get_own_key_data() is None: ErrorDialog( _('No OpenPGP key assigned'), _('No OpenPGP key is assigned to your account.')) chat_control.sendmessage = False def _encrypt_message(self, conn, event, callback): account = conn.name self.get_pgp_module(account).encrypt_message(conn, event, callback) def encrypt_file(self, file, account, callback): self.get_pgp_module(account).encrypt_file(file, callback) @staticmethod def _on_file_encryption_error(event): ErrorDialog(_('Error'), event.error)