openpgp/0000755000175500017550000000000013515434436012270 5ustar debacledebacleopenpgp/gtk/0000755000175500017550000000000013515434436013055 5ustar debacledebacleopenpgp/gtk/key.py0000644000175500017550000002016113515434436014217 0ustar debacledebacle# Copyright (C) 2018 Philipp Hörist # # 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 . # 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 gajim.plugins.plugins_i18n import _ 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) openpgp/gtk/wizard.py0000644000175500017550000001705613515434436014740 0ustar debacledebacle# Copyright (C) 2018 Philipp Hörist # # 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 . # 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 from gajim.plugins.plugins_i18n import _ 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) openpgp/gtk/__init__.py0000644000175500017550000000000013515434436015154 0ustar debacledebacleopenpgp/gtk/style.css0000644000175500017550000000112313515434436014724 0ustar debacledebacle.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; } openpgp/modules/0000755000175500017550000000000013515434436013740 5ustar debacledebacleopenpgp/modules/util.py0000644000175500017550000001427513515434436015300 0ustar debacledebacle# Copyright (C) 2018 Philipp Hörist # # 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 . # 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 from gajim.common.modules.date_and_time import parse_datetime ENCRYPTION_NAME = 'OpenPGP' 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) if date is None: log.warning('No date in metadata') return timestamp = parse_datetime(date, epoch=True) if timestamp is None: log.warning('Invalid date timestamp: %s', date) return fingerprints.append( Key(attrs['v4-fingerprint'], int(timestamp))) 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): ''' ''' 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).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`]' def add_additional_data(data, fingerprint): data['encrypted'] = {'name': ENCRYPTION_NAME, 'fingerprint': fingerprint} class VerifyFailed(Exception): pass class DecryptionFailed(Exception): pass openpgp/modules/openpgp.py0000644000175500017550000004445313515434436015774 0ustar debacledebacle# Copyright (C) 2018 Philipp Hörist # # 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 . # 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 ENCRYPTION_NAME from openpgp.modules.util import add_additional_data 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 DecryptionFailed from openpgp.backend.sql import Storage from openpgp.backend.pygpg import PGPContext log = logging.getLogger('gajim.plugin_system.openpgp') # Module name name = ENCRYPTION_NAME 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.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 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() log.info('Key list query received from %s', from_jid) 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, self._date)) 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, fingerprint = 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 add_additional_data(obj.additional_data, fingerprint) 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) add_additional_data(obj.additional_data, self._fingerprint) 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' openpgp/modules/pgp_keylist.py0000644000175500017550000000755113515434436016654 0ustar debacledebacle# Copyright (C) 2018 Philipp Hörist # # 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 . # 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): ''' ''' 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'], int(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' openpgp/modules/__init__.py0000644000175500017550000000000013515434436016037 0ustar debacledebacleopenpgp/pgpplugin.py0000644000175500017550000001553313515434436014656 0ustar debacledebacle# Copyright (C) 2018 Philipp Hörist # # 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 . # 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 gajim.plugins.plugins_i18n import _ from openpgp.modules.util import NS_NOTIFY from openpgp.modules.util import ENCRYPTION_NAME 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 = ENCRYPTION_NAME 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 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') 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: con.get_module('OpenPGP').query_key_list(jid) 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) openpgp/backend/0000755000175500017550000000000013515434436013657 5ustar debacledebacleopenpgp/backend/sql.py0000644000175500017550000000641113515434436015032 0ustar debacledebacle# Copyright (C) 2018 Philipp Hörist # # 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 . # 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(str(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() openpgp/backend/pygpg.py0000644000175500017550000001340313515434436015360 0ustar debacledebacle# Copyright (C) 2018 Philipp Hörist # # 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 . # 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, armor=False, sign=self._own_fingerprint, always_trust=True, passphrase=self._passphrase) if result.ok: error = '' else: error = result.status return result.data, 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'), 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.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) openpgp/backend/gpgme.py0000644000175500017550000001463213515434436015336 0ustar debacledebacle# Copyright (C) 2018 Philipp Hörist # # 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 . # 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 = """ 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 """ 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') openpgp/backend/__init__.py0000644000175500017550000000000013515434436015756 0ustar debacledebacleopenpgp/__init__.py0000644000175500017550000000004513515434436014400 0ustar debacledebaclefrom .pgpplugin import OpenPGPPlugin openpgp/manifest.ini0000644000175500017550000000042313515434436014576 0ustar debacledebacle[info] name: OpenPGP short_name: openpgp version: 1.1.2 description: Experimental OpenPGP XEP-0373 Implementation authors: Philipp Hörist homepage: https://dev.gajim.org/gajim/gajim-plugins/wikis/pgp min_gajim_version: 1.0.90 max_gajim_version: 1.1.99