openpgp/0000755000175500017550000000000013772217733012274 5ustar debacledebacleopenpgp/gtk/0000755000175500017550000000000013772217733013061 5ustar debacledebacleopenpgp/gtk/key.py0000644000175500017550000002003113772217733014217 0ustar debacledebacle# Copyright (C) 2019 Philipp Hörist # # This file is part of the OpenPGP Gajim Plugin. # # OpenPGP 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. # # OpenPGP 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 OpenPGP Gajim Plugin. If not, see . import logging import time from gi.repository import Gtk from gajim.common import app from gajim.gui.dialogs import ConfirmationDialog from gajim.gui.dialogs import DialogButton from gajim.plugins.plugins_i18n import _ from openpgp.modules.util import Trust log = logging.getLogger('gajim.p.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'), 'encrypted-color'), Trust.VERIFIED: ('security-high-symbolic', _('Verified'), 'encrypted-color') } class KeyDialog(Gtk.Dialog): def __init__(self, account, jid, transient): super().__init__(title=_('Public Keys for %s') % jid, destroy_with_parent=True) 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() ConfirmationDialog( _('Delete'), _('Delete Public Key'), _('This will permanently delete this public key'), [DialogButton.make('Cancel'), DialogButton.make('Remove', text=_('Delete'), callback=_remove)], transient_for=self.get_toplevel()).show() 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 = 'encrypted-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) openpgp/gtk/wizard.py0000644000175500017550000001670713772217733014746 0ustar debacledebacle# Copyright (C) 2019 Philipp Hörist # # This file is part of the OpenPGP Gajim Plugin. # # OpenPGP 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. # # OpenPGP 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 OpenPGP Gajim Plugin. If not, see . import logging import threading from enum import IntEnum from gi.repository import Gtk from gi.repository import GLib from gajim.common import app from gajim.plugins.plugins_i18n import _ log = logging.getLogger('gajim.p.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_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): text = None try: self._con.get_module('OpenPGP').generate_key() except Exception as error: text = str(error) GLib.idle_add(self.finished, text) def finished(self, error): if error is None: self._assistant.set_current_page(Page.SUCCESS) self._con.get_module('OpenPGP').get_own_key_details() self._con.get_module('OpenPGP').set_public_key() self._con.get_module('OpenPGP').request_keylist() else: error_page = self._assistant.get_nth_page(Page.ERROR) error_page.set_text(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 = _('Setup 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) openpgp/gtk/__init__.py0000644000175500017550000000000013772217733015160 0ustar debacledebacleopenpgp/gtk/style.css0000644000175500017550000000102413772217732014727 0ustar debacledebacle.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; } openpgp/modules/0000755000175500017550000000000013772217733013744 5ustar debacledebacleopenpgp/modules/util.py0000644000175500017550000000374513772217733015304 0ustar debacledebacle# Copyright (C) 2019 Philipp Hörist # # This file is part of the OpenPGP Gajim Plugin. # # OpenPGP 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. # # OpenPGP 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 OpenPGP Gajim Plugin. If not, see . from enum import IntEnum from collections import namedtuple from nbxmpp.namespaces import Namespace ENCRYPTION_NAME = 'OpenPGP' NOT_ENCRYPTED_TAGS = [ ('no-store', Namespace.HINTS), ('store', Namespace.HINTS), ('no-copy', Namespace.HINTS), ('no-permanent-store', Namespace.HINTS), ('origin-id', Namespace.SID), ('thread', None) ] Key = namedtuple('Key', 'fingerprint date') class Trust(IntEnum): NOT_TRUSTED = 0 UNKNOWN = 1 BLIND = 2 VERIFIED = 3 def prepare_stanza(stanza, payload): delete_nodes(stanza, 'openpgp', Namespace.OPENPGP) delete_nodes(stanza, 'body') nodes = [(node.getName(), node.getNamespace()) for node in payload] for name, namespace in nodes: delete_nodes(stanza, name, namespace) for node in payload: stanza.addChild(node=node) def delete_nodes(stanza, name, namespace=None): attrs = None if namespace is not None: attrs = {'xmlns': Namespace.OPENPGP} nodes = stanza.getTags(name, attrs) for node in nodes: stanza.delChild(node) def add_additional_data(data, fingerprint): data['encrypted'] = {'name': ENCRYPTION_NAME, 'fingerprint': fingerprint} class VerifyFailed(Exception): pass class DecryptionFailed(Exception): pass openpgp/modules/openpgp.py0000644000175500017550000002440513772217733015773 0ustar debacledebacle# Copyright (C) 2019 Philipp Hörist # # This file is part of the OpenPGP Gajim Plugin. # # OpenPGP 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. # # OpenPGP 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 OpenPGP Gajim Plugin. If not, see . import sys import time import logging from pathlib import Path from nbxmpp.namespaces import Namespace from nbxmpp import Node from nbxmpp import StanzaMalformed from nbxmpp.structs import StanzaHandler from nbxmpp.errors import StanzaError from nbxmpp.errors import MalformedStanzaError from nbxmpp.modules.openpgp import PGPKeyMetadata from nbxmpp.modules.openpgp import parse_signcrypt from nbxmpp.modules.openpgp import create_signcrypt_node from nbxmpp.modules.openpgp import create_message_stanza from gajim.common import app from gajim.common import configpaths from gajim.common.nec import NetworkEvent from gajim.common.const import EncryptionData from gajim.common.modules.base import BaseModule from gajim.common.modules.util import event_node from openpgp.modules.util import ENCRYPTION_NAME from openpgp.modules.util import NOT_ENCRYPTED_TAGS from openpgp.modules.util import Key from openpgp.modules.util import add_additional_data from openpgp.modules.util import DecryptionFailed from openpgp.modules.util import prepare_stanza from openpgp.modules.key_store import PGPContacts from openpgp.backend.sql import Storage if sys.platform == 'win32': from openpgp.backend.pygpg import PythonGnuPG as PGPBackend else: from openpgp.backend.gpgme import GPGME as PGPBackend log = logging.getLogger('gajim.p.openpgp') # Module name name = ENCRYPTION_NAME zeroconf = False class OpenPGP(BaseModule): _nbxmpp_extends = 'OpenPGP' _nbxmpp_methods = [ 'set_keylist', 'request_keylist', 'set_public_key', 'request_public_key', 'set_secret_key', 'request_secret_key', ] def __init__(self, con): BaseModule.__init__(self, con) self.handlers = [ StanzaHandler(name='message', callback=self.decrypt_message, ns=Namespace.OPENPGP, priority=9), ] self._register_pubsub_handler(self._keylist_notification_received) self.own_jid = self._con.get_own_jid() own_bare_jid = self.own_jid.getBare() path = Path(configpaths.get('MY_DATA')) / 'openpgp' / own_bare_jid if not path.exists(): path.mkdir(mode=0o700, parents=True) self._pgp = PGPBackend(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_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 set_public_key(self): log.info('%s => Publish public key', self._account) key = self._pgp.export_key(self._fingerprint) self._nbxmpp('OpenPGP').set_public_key( key, self._fingerprint, self._date) def request_public_key(self, jid, fingerprint): log.info('%s => Request public key %s - %s', self._account, fingerprint, jid) self._nbxmpp('OpenPGP').request_public_key( jid, fingerprint, callback=self._public_key_received, user_data=fingerprint) def _public_key_received(self, task): fingerprint = task.get_user_data() try: result = task.finish() except (StanzaError, MalformedStanzaError) as error: log.error('%s => Public Key not found: %s', self._account, error) return imported_key = self._pgp.import_key(result.key, result.jid) if imported_key is not None: self._contacts.set_public_key(result.jid, fingerprint) def set_keylist(self, keylist=None): if keylist is None: keylist = [PGPKeyMetadata(None, self._fingerprint, self._date)] log.info('%s => Publish keylist', self._account) self._nbxmpp('OpenPGP').set_keylist(keylist) @event_node(Namespace.OPENPGP_PK) def _keylist_notification_received(self, _con, _stanza, properties): if properties.pubsub_event.retracted: return keylist = properties.pubsub_event.data or [] self._process_keylist(keylist, properties.jid) def request_keylist(self, jid=None): if jid is None: jid = self.own_jid log.info('%s => Fetch keylist %s', self._account, jid) self._nbxmpp('OpenPGP').request_keylist( jid, callback=self._keylist_received, user_data=jid) def _keylist_received(self, task): jid = task.get_user_data() try: keylist = task.finish() except (StanzaError, MalformedStanzaError) as error: log.error('%s => Keylist query failed: %s', self._account, error) if self.own_jid.bareMatch(jid) and self._fingerprint is not None: self.set_keylist() return log.info('Keylist received from %s', jid) self._process_keylist(keylist, jid) def _process_keylist(self, keylist, from_jid): if not keylist: log.warning('%s => Empty keylist received from %s', self._account, from_jid) self._contacts.process_keylist(self.own_jid, keylist) if self.own_jid.bareMatch(from_jid) and self._fingerprint is not None: self.set_keylist() return if self.own_jid.bareMatch(from_jid): log.info('Received own keylist') 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, self._date)) self.set_keylist(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.request_public_key(from_jid, fingerprint) def decrypt_message(self, _con, stanza, properties): if not properties.is_openpgp: return try: payload, fingerprint = self._pgp.decrypt(properties.openpgp) except DecryptionFailed as error: log.warning(error) return signcrypt = Node(node=payload) try: payload, recipients, _timestamp = parse_signcrypt(signcrypt) except StanzaMalformed as error: log.warning('Decryption failed: %s', error) log.warning(payload) return if not any(map(self.own_jid.bareMatch, recipients)): log.warning('to attr not valid') log.warning(signcrypt) return keys = self._contacts.get_keys(properties.jid.bare) fingerprints = [key.fingerprint for key in keys] if fingerprint not in fingerprints: log.warning('Invalid fingerprint on message: %s', fingerprint) log.warning('Expected: %s', fingerprints) return log.info('Received OpenPGP message from: %s', properties.jid) prepare_stanza(stanza, payload) properties.encrypted = EncryptionData({'name': ENCRYPTION_NAME, 'fingerprint': fingerprint}) def encrypt_message(self, obj, callback): keys = self._contacts.get_keys(obj.jid) if not keys: 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 = create_signcrypt_node(obj.stanza, [obj.jid], NOT_ENCRYPTED_TAGS) encrypted_payload, error = self._pgp.encrypt(payload, keys) if error: log.error('Error: %s', error) app.nec.push_incoming_event( NetworkEvent('message-not-sent', conn=self._con, jid=obj.jid, message=obj.message, error=error, time_=time.time(), session=None)) return create_message_stanza(obj.stanza, encrypted_payload, bool(obj.message)) add_additional_data(obj.additional_data, self._fingerprint) obj.encrypted = ENCRYPTION_NAME 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.set_keylist() def cleanup(self): self._storage.cleanup() self._pgp = None self._contacts = None def get_instance(*args, **kwargs): return OpenPGP(*args, **kwargs), 'OpenPGP' openpgp/modules/key_store.py0000644000175500017550000002037413772217733016330 0ustar debacledebacle# Copyright (C) 2019 Philipp Hörist # # This file is part of the OpenPGP Gajim Plugin. # # OpenPGP 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. # # OpenPGP 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 OpenPGP Gajim Plugin. If not, see . import logging from openpgp.modules.util import Trust log = logging.getLogger('gajim.p.openpgp.store') 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.values(): 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: %s %s', 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 get_key(self, fingerprint): return self._key_store.get(fingerprint, None) 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 [] def get_trust(self, jid, fingerprint): contact_data = self._contacts.get(jid, None) if contact_data is None: return Trust.UNKNOWN key = contact_data.get_key(fingerprint) if key is None: return Trust.UNKNOWN return key.trust openpgp/modules/__init__.py0000644000175500017550000000000013772217733016043 0ustar debacledebacleopenpgp/pgpplugin.py0000644000175500017550000001452013772217733014655 0ustar debacledebacle# Copyright (C) 2019 Philipp Hörist # # This file is part of the OpenPGP Gajim Plugin. # # OpenPGP 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. # # OpenPGP 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 OpenPGP Gajim Plugin. If not, see . import logging import os from pathlib import Path from gi.repository import Gtk from gi.repository import Gdk from nbxmpp.namespaces import Namespace from nbxmpp import JID 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.gui.dialogs import ErrorDialog from gajim.plugins import GajimPlugin from gajim.plugins.plugins_i18n import _ from openpgp.modules.util import ENCRYPTION_NAME try: from openpgp.modules import openpgp except ImportError as e: ERROR_MSG = str(e) else: ERROR_MSG = None log = logging.getLogger('gajim.p.openpgp') 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 = [openpgp] self.encryption_name = ENCRYPTION_NAME self.config_dialog = None self.gui_extension_points = { 'encrypt' + self.encryption_name: (self._encrypt_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 path.open('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') @staticmethod def _create_paths(): keyring_path = Path(configpaths.get('MY_DATA')) / 'openpgp' if not keyring_path.exists(): keyring_path.mkdir() 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').request_keylist() con.get_module('OpenPGP').set_public_key() def activate(self): for account in app.connections: con = app.connections[account] con.get_module('Caps').update_caps() 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').request_keylist() con.get_module('OpenPGP').set_public_key() def deactivate(self): pass @staticmethod def _update_caps(_account, features): features.append('%s+notify' % Namespace.OPENPGP_PK) 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: con.get_module('OpenPGP').request_keylist(JID(jid)) ErrorDialog( _('No OpenPGP key'), _('We didnt receive a OpenPGP key from this contact.')) return return True from openpgp.gtk.wizard import KeyWizard KeyWizard(self, account, chat_control) return False @staticmethod def encryption_state(_chat_control, state): state['authenticated'] = True state['visible'] = True @staticmethod def on_encryption_button_clicked(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 @staticmethod def _encrypt_message(con, obj, callback): if not con.get_module('OpenPGP').secret_key_available: return con.get_module('OpenPGP').encrypt_message(obj, callback) openpgp/backend/0000755000175500017550000000000013772217733013663 5ustar debacledebacleopenpgp/backend/sql.py0000644000175500017550000000667113772217733015046 0ustar debacledebacle# Copyright (C) 2019 Philipp Hörist # # This file is part of the OpenPGP Gajim Plugin. # # OpenPGP 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. # # OpenPGP 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 OpenPGP Gajim Plugin. If not, see . import sqlite3 import logging from collections import namedtuple log = logging.getLogger('gajim.p.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(str(folder_path / 'contacts.db'), detect_types=sqlite3.PARSE_COLNAMES) 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 jid as "jid [jid]", fingerprint, active, trust, timestamp, comment FROM contacts''' return self._con.execute(sql).fetchall() 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() openpgp/backend/pygpg.py0000644000175500017550000001313513772217733015366 0ustar debacledebacle# Copyright (C) 2019 Philipp Hörist # # This file is part of the OpenPGP Gajim Plugin. # # OpenPGP 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. # # OpenPGP 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 OpenPGP Gajim Plugin. If not, see . import os import logging from collections import namedtuple import gnupg from openpgp.modules.util import DecryptionFailed log = logging.getLogger('gajim.p.openpgp.pygnupg') if log.getEffectiveLevel() == logging.DEBUG: log = logging.getLogger('gnupg') log.addHandler(logging.StreamHandler()) log.setLevel(logging.DEBUG) KeyringItem = namedtuple('KeyringItem', 'jid keyid fingerprint') class PythonGnuPG(gnupg.GPG): def __init__(self, jid, gnupghome): gnupg.GPG.__init__(self, gpgbinary='gpg', gnupghome=str(gnupghome)) self._jid = jid.getBare() self._own_fingerprint = None @staticmethod def _get_key_params(jid): ''' Generate --gen-key input ''' params = { 'Key-Type': 'RSA', 'Key-Length': 2048, 'Name-Real': 'xmpp:%s' % jid, } out = 'Key-Type: %s\n' % params.pop('Key-Type') for key, val in list(params.items()): out += '%s: %s\n' % (key, val) out += '%no-protection\n' out += '%commit\n' return out def generate_key(self): super().gen_key(self._get_key_params(self._jid)) 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, armor=False, sign=self._own_fingerprint, always_trust=True) if result.ok: error = '' else: error = result.status return result.data, error def decrypt(self, payload): result = super().decrypt(payload, always_trust=True) if not result.ok: raise DecryptionFailed(result.status) return result.data.decode('utf8'), result.fingerprint 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) return if not self.validate_key(data, str(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: key_found = False 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=True) return key def delete_key(self, fingerprint): log.info('Delete Key: %s', fingerprint) super().delete_keys(fingerprint) openpgp/backend/gpgme.py0000644000175500017550000001343713772217733015344 0ustar debacledebacle# Copyright (C) 2019 Philipp Hörist # # This file is part of the OpenPGP Gajim Plugin. # # OpenPGP 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. # # OpenPGP 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 OpenPGP Gajim Plugin. If not, see . import logging from nbxmpp.protocol import JID import gpg from gpg.results import ImportResult from openpgp.modules.util import DecryptionFailed log = logging.getLogger('gajim.p.openpgp.gpgme') class KeyringItem: def __init__(self, key): self._key = key self._uid = self._get_uid() def _get_uid(self): for uid in self._key.uids: if uid.uid.startswith('xmpp:'): return uid.uid @property def fingerprint(self): return self._key.fpr @property def uid(self): if self._uid is not None: return self._uid @property def jid(self): if self._uid is not None: return JID.from_string(self._uid[5:]) def __hash__(self): return hash(self.fingerprint) class GPGME: def __init__(self, jid, gnuhome): self._jid = jid self._context_args = { 'home_dir': str(gnuhome), 'offline': True, 'armor': False, } def generate_key(self): with gpg.Context(**self._context_args) as context: result = context.create_key(f'xmpp:{str(self._jid)}', expires=False, sign=True, encrypt=True, certify=False, authenticate=False, passphrase=None, force=False) log.info('Generated new key: %s', result.fpr) def get_key(self, fingerprint): with gpg.Context(**self._context_args) as context: try: key = context.get_key(fingerprint) except gpg.errors.KeyNotFound as error: log.warning('key not found: %s', error.keystr) return except Exception as error: log.warning('get_key() error: %s', error) return return key def get_own_key_details(self): with gpg.Context(**self._context_args) as context: keys = list(context.keylist(secret=True)) if not keys: return None, None key = keys[0] for subkey in key.subkeys: if subkey.fpr == key.fpr: return subkey.fpr, subkey.timestamp return None, None def get_keys(self): keys = [] with gpg.Context(**self._context_args) as context: for key in context.keylist(): keys.append(KeyringItem(key)) return keys def export_key(self, fingerprint): with gpg.Context(**self._context_args) as context: key = context.key_export_minimal(pattern=fingerprint) return key # 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, plaintext, keys): recipients = [] with gpg.Context(**self._context_args) as context: for key in keys: key = context.get_key(key.fingerprint) if key is not None: recipients.append(key) if not recipients: return None, 'No keys found to encrypt to' with gpg.Context(**self._context_args) as context: result = context.encrypt(str(plaintext).encode(), recipients, always_trust=True) ciphertext, result, _sign_result = result return ciphertext, None def decrypt(self, ciphertext): with gpg.Context(**self._context_args) as context: try: result = context.decrypt(ciphertext) except Exception as error: raise DecryptionFailed('Decryption failed: %s' % error) plaintext, result, verify_result = result plaintext = plaintext.decode() fingerprints = [sig.fpr for sig in verify_result.signatures] if not fingerprints or len(fingerprints) > 1: log.error(result) log.error(verify_result) raise DecryptionFailed('Verification failed') return plaintext, fingerprints[0] def import_key(self, data, jid): log.info('Import key from %s', jid) with gpg.Context(**self._context_args) as context: result = context.key_import(data) if not isinstance(result, ImportResult) or result.imported != 1: log.error('Key import failed: %s', jid) log.error(result) return fingerprint = result.imports[0].fpr key = self.get_key(fingerprint) return KeyringItem(key) openpgp/backend/__init__.py0000644000175500017550000000000013772217733015762 0ustar debacledebacleopenpgp/__init__.py0000644000175500017550000000004513772217733014404 0ustar debacledebaclefrom .pgpplugin import OpenPGPPlugin openpgp/manifest.ini0000644000175500017550000000042613772217733014605 0ustar debacledebacle[info] name: OpenPGP short_name: openpgp version: 1.3.9 description: Experimental OpenPGP (XEP-0373) implementation. authors: Philipp Hörist homepage: https://dev.gajim.org/gajim/gajim-plugins/wikis/pgp min_gajim_version: 1.2.91 max_gajim_version: 1.3.90 openpgp/org.gajim.Gajim.Plugin.openpgp.metainfo.xml0000644000175500017550000000071213772217732022425 0ustar debacledebacle org.gajim.Gajim.Plugin.openpgp org.gajim.Gajim OpenPGP Plugin Experimental OpenPGP XEP-0373 Implementation https://gajim.org/ CC-BY-SA-3.0 GPL-3.0 gajim-devel_AT_gajim.org