Commit 466a4e91 authored by Philipp Hörist's avatar Philipp Hörist

[pgp] Move all Gajim PGP code into plugin

parent b35a2599
from .pgpplugin import OldPGPPlugin
from .plugin import PGPPlugin
# Copyright (C) 2019 Philipp Hörist <philipp AT hoerist.com>
# Copyright (C) 2003-2014 Yann Leboulanger <asterix AT lagaule.org>
# Copyright (C) 2005 Alex Mauer <hawke AT hawkesnest.net>
# Copyright (C) 2005-2006 Nikos Kouremenos <kourem AT gmail.com>
# Copyright (C) 2007 Stephan Erb <steve-e AT h3c.de>
# Copyright (C) 2008 Jean-Marie Traissard <jim AT lapin.org>
# Jonathan Schleifer <js-gajim AT webkeks.org>
#
# This file is part of PGP Gajim Plugin.
#
# PGP Gajim Plugin is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# PGP Gajim Plugin is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with PGP Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
import os
import logging
from functools import lru_cache
import gnupg
from gajim.common.helpers import Singleton
from pgp.exceptions import SignError
logger = logging.getLogger('gajim.p.pgplegacy')
if logger.getEffectiveLevel() == logging.DEBUG:
logger = logging.getLogger('gnupg')
logger.addHandler(logging.StreamHandler())
logger.setLevel(logging.DEBUG)
class PGP(gnupg.GPG, metaclass=Singleton):
def __init__(self, binary, encoding=None):
super().__init__(gpgbinary=binary,
use_agent=True)
if encoding is not None:
self.encoding = encoding
self.decode_errors = 'replace'
def encrypt(self, payload, recipients, always_trust=False):
if not always_trust:
# check that we'll be able to encrypt
result = self.get_key(recipients[0])
for key in result:
if key['trust'] not in ('f', 'u'):
return '', 'NOT_TRUSTED ' + key['keyid'][-8:]
result = super().encrypt(
payload.encode('utf8'),
recipients,
always_trust=always_trust)
if result.ok:
error = ''
else:
error = result.status
return self._strip_header_footer(str(result)), error
def decrypt(self, payload):
data = self._add_header_footer(payload, 'MESSAGE')
result = super().decrypt(data.encode('utf8'))
return result.data.decode('utf8')
@lru_cache(maxsize=8)
def sign(self, payload, key_id):
if payload is None:
payload = ''
result = super().sign(payload.encode('utf8'),
keyid=key_id,
detach=True)
if result.fingerprint:
return self._strip_header_footer(str(result))
raise SignError(result.status)
def verify(self, payload, signed):
# Hash algorithm is not transfered in the signed
# presence stanza so try all algorithms.
# Text name for hash algorithms from RFC 4880 - section 9.4
if payload is None:
payload = ''
hash_algorithms = ['SHA512', 'SHA384', 'SHA256',
'SHA224', 'SHA1', 'RIPEMD160']
for algo in hash_algorithms:
data = os.linesep.join(
['-----BEGIN PGP SIGNED MESSAGE-----',
'Hash: ' + algo,
'',
payload,
self._add_header_footer(signed, 'SIGNATURE')]
)
result = super().verify(data.encode('utf8'))
if result.valid:
return result.key_id
def get_key(self, key_id):
return super().list_keys(keys=[key_id])
def get_keys(self, secret=False):
keys = {}
result = super().list_keys(secret=secret)
for key in result:
# Take first not empty uid
keys[key['keyid'][8:]] = [uid for uid in key['uids'] if uid][0]
return keys
@staticmethod
def _strip_header_footer(data):
"""
Remove header and footer from data
"""
if not data:
return ''
lines = data.splitlines()
while lines[0] != '':
lines.remove(lines[0])
while lines[0] == '':
lines.remove(lines[0])
i = 0
for line in lines:
if line:
if line[0] == '-':
break
i = i+1
line = '\n'.join(lines[0:i])
return line
@staticmethod
def _add_header_footer(data, type_):
"""
Add header and footer from data
"""
out = "-----BEGIN PGP %s-----" % type_ + os.linesep
out = out + "Version: PGP" + os.linesep
out = out + os.linesep
out = out + data + os.linesep
out = out + "-----END PGP %s-----" % type_ + os.linesep
return out
# Copyright (C) 2019 Philipp Hörist <philipp AT hoerist.com>
#
# This file is part of the PGP Gajim Plugin.
#
# PGP Gajim Plugin is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# PGP Gajim Plugin is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with PGP Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
import json
from pathlib import Path
from gajim.common import app
from gajim.common import configpaths
from gajim.common.helpers import delay_execution
class KeyStore:
def __init__(self, account, own_jid, log):
self._log = log
self._account = account
self._store = {
'own_key_data': None,
'contact_key_data': {},
}
own_bare_jid = own_jid.getBare()
path = Path(configpaths.get('PLUGINS_DATA')) / 'pgplegacy' / own_bare_jid
if not path.exists():
path.mkdir(parents=True)
self._store_path = path / 'store'
if self._store_path.exists():
with self._store_path.open('r') as file:
try:
self._store = json.load(file)
except Exception:
log.exception('Could not load config')
if not self._store['contact_key_data']:
self._migrate()
def _migrate(self):
keys = {}
attached_keys = app.config.get_per(
'accounts', self._account, 'attached_gpg_keys').split()
if attached_keys is None:
return
for i in range(len(attached_keys) // 2):
keys[attached_keys[2 * i]] = attached_keys[2 * i + 1]
for jid, key_id in keys.items():
self.set_contact_key_data(jid, (key_id, ''))
own_key_id = app.config.get_per('accounts', self._account, 'keyid')
own_key_user = app.config.get_per('accounts', self._account, 'keyname')
if own_key_id:
self.set_own_key_data((own_key_id, own_key_user))
self._log.info('Migration successful')
@delay_execution(500)
def _save_store(self):
with self._store_path.open('w') as file:
json.dump(self._store, file)
def _get_dict_key(self, jid):
return '%s-%s' % (self._account, jid)
def set_own_key_data(self, key_data):
if key_data is None:
self._store['own_key_data'] = None
else:
self._store['own_key_data'] = {
'key_id': key_data[0],
'key_user': key_data[1]
}
self._save_store()
def get_own_key_data(self):
return self._store['own_key_data']
def get_contact_key_data(self, jid):
key_ids = self._store['contact_key_data']
dict_key = self._get_dict_key(jid)
return key_ids.get(dict_key)
def set_contact_key_data(self, jid, key_data):
key_ids = self._store['contact_key_data']
dict_key = self._get_dict_key(jid)
if key_data is None:
self._store['contact_key_data'][dict_key] = None
else:
key_ids[dict_key] = {
'key_id': key_data[0],
'key_user': key_data[1]
}
self._save_store()
# Copyright (C) 2019 Philipp Hörist <philipp AT hoerist.com>
#
# This file is part of the PGP Gajim Plugin.
#
# PGP Gajim Plugin is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# PGP Gajim Plugin is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with PGP Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
class SignError(Exception):
pass
class KeyMismatch(Exception):
pass
class NoKeyIdFound(Exception):
pass
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.1 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<object class="GtkListStore" id="liststore">
<columns>
<!-- column-name keyid -->
<column type="gchararray"/>
<!-- column-name contactname -->
<column type="gchararray"/>
</columns>
</object>
<object class="GtkBox" id="box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="border_width">6</property>
<property name="orientation">vertical</property>
<property name="spacing">6</property>
<child>
<object class="GtkScrolledWindow">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="vexpand">True</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkTreeView" id="keys_treeview">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="model">liststore</property>
<property name="search_column">1</property>
<signal name="cursor-changed" handler="_on_row_changed" swapped="no"/>
<child internal-child="selection">
<object class="GtkTreeSelection"/>
</child>
<child>
<object class="GtkTreeViewColumn">
<property name="title" translatable="yes">Key ID</property>
<property name="sort_order">descending</property>
<child>
<object class="GtkCellRendererText"/>
<attributes>
<attribute name="text">0</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn">
<property name="title" translatable="yes">Contact Name</property>
<property name="sort_column_id">1</property>
<child>
<object class="GtkCellRendererText"/>
<attributes>
<attribute name="text">1</attribute>
</attributes>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</interface>
# Copyright (C) 2019 Philipp Hörist <philipp AT hoerist.com>
#
# This file is part of the PGP Gajim Plugin.
#
# PGP Gajim Plugin is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# PGP Gajim Plugin is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with PGP Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
from pathlib import Path
from gi.repository import Gtk
from gi.repository import Gdk
from gajim.common import app
from gajim.plugins.helpers import get_builder
from gajim.plugins.plugins_i18n import _
from pgp.gtk.key import ChooseGPGKeyDialog
class PGPConfigDialog(Gtk.ApplicationWindow):
def __init__(self, plugin, parent):
Gtk.ApplicationWindow.__init__(self)
self.set_application(app.app)
self.set_show_menubar(False)
self.set_title(_('PGP Configuration'))
self.set_transient_for(parent)
self.set_resizable(True)
self.set_type_hint(Gdk.WindowTypeHint.DIALOG)
self.set_destroy_with_parent(True)
ui_path = Path(__file__).parent
self._ui = get_builder(ui_path.resolve() / 'config.ui')
self.add(self._ui.config_box)
self._ui.connect_signals(self)
self._plugin = plugin
for account in app.connections.keys():
page = Page(plugin, account)
self._ui.stack.add_titled(page,
account,
app.get_account_label(account))
self.show_all()
class Page(Gtk.Box):
def __init__(self, plugin, account):
Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL)
self._con = app.connections[account]
self._plugin = plugin
self._label = Gtk.Label()
self._button = Gtk.Button(label=_('Assign Key'))
self._button.connect('clicked', self._on_assign)
self._load_key()
self.add(self._label)
self.add(self._button)
self.show_all()
def _on_assign(self, _button):
backend = self._con.get_module('PGPLegacy').pgp_backend
secret_keys = backend.get_keys(secret=True)
dialog = ChooseGPGKeyDialog(secret_keys, self.get_toplevel())
dialog.connect('response', self._on_response)
def _load_key(self):
key_data = self._con.get_module('PGPLegacy').get_own_key_data()
if key_data is None:
self._set_key(None)
else:
self._set_key((key_data['key_id'], key_data['key_user']))
def _on_response(self, dialog, response):
if response != Gtk.ResponseType.OK:
return
if dialog.selected_key is None:
self._con.get_module('PGPLegacy').set_own_key_data(None)
self._set_key(None)
else:
self._con.get_module('PGPLegacy').set_own_key_data(
dialog.selected_key)
self._set_key(dialog.selected_key)
def _set_key(self, key_data):
if key_data is None:
self._label.set_text(_('No key assigned'))
else:
key_id, key_user = key_data
self._label.set_text('%s %s' % (key_id, key_user))
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.1 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<object class="GtkBox" id="config_box">
<property name="width_request">500</property>
<property name="height_request">400</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">12</property>
<child>
<object class="GtkStackSidebar" id="sidebar">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stack">stack</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkStack" id="stack">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="transition_type">crossfade</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</interface>
# Copyright (C) 2019 Philipp Hörist <philipp AT hoerist.com>
#
# This file is part of the PGP Gajim Plugin.
#
# PGP Gajim Plugin is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# PGP Gajim Plugin is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with PGP Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
from pathlib import Path
from gi.repository import Gtk
from gajim.common import app
from gajim.plugins.plugins_i18n import _
from gajim.plugins.helpers import get_builder
class KeyDialog(Gtk.Dialog):
def __init__(self, plugin, account, jid, transient):
super().__init__(title=_('Assign key for %s') % jid,
destroy_with_parent=True)
self.set_transient_for(transient)
self.set_resizable(True)
self.set_default_size(500, 300)
self._plugin = plugin
self._jid = jid
self._con = app.connections[account]
self._label = Gtk.Label()
self._assign_button = Gtk.Button(label='assign')
self._assign_button.connect('clicked', self._choose_key)
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
box.add(self._label)
box.add(self._assign_button)
area = self.get_content_area()
area.pack_start(box, True, True, 0)
self._load_key()
self.show_all()
def _choose_key(self, *args):
backend = self._con.get_module('PGPLegacy').pgp_backend
dialog = ChooseGPGKeyDialog(backend.get_keys(), self)
dialog.connect('response', self._on_response)
def _load_key(self):
key_data = self._con.get_module('PGPLegacy').get_contact_key_data(
self._jid)
if key_data is None:
self._set_key(None)
else:
self._set_key(key_data.values())
def _on_response(self, dialog, response):
if response != Gtk.ResponseType.OK:
return
if dialog.selected_key is None:
self._con.get_module('PGPLegacy').set_contact_key_data(
self._jid, None)
self._set_key(None)
else:
self._con.get_module('PGPLegacy').set_contact_key_data(
self._jid, dialog.selected_key)
self._set_key(dialog.selected_key)
def _set_key(self, key_data):
if key_data is None:
self._label.set_text(_('No key assigned'))
else:
key_id, key_user = key_data
self._label.set_text('%s %s' % (key_id, key_user))
class ChooseGPGKeyDialog(Gtk.Dialog):
def __init__(self, secret_keys, transient_for):
Gtk.Dialog.__init__(self,
title=_('Assign PGP Key'),
transient_for=transient_for)
secret_keys[_('None')] = _('None')
self.set_position(Gtk.WindowPosition.CENTER_ON_PARENT)
self.set_resizable(True)
self.set_default_size(500, 300)
self.add_button(_('OK'), Gtk.ResponseType.OK)
self.add_button(_('Cancel'), Gtk.ResponseType.CANCEL)
self._selected_key = None
ui_path = Path(__file__).parent
self._ui = get_builder(ui_path.resolve() / 'choose_key.ui')
self._ui.keys_treeview = self._ui.keys_treeview
model = self._ui.keys_treeview.get_model()
model.set_sort_func(1, self._sort)
model = self._ui.keys_treeview.get_model()
for key_id in secret_keys.keys():
model.append((key_id, secret_keys[key_id]))
self.get_content_area().add(self._ui.box)
self._ui.connect_signals(self)
self.connect_after('response', self._on_response)
self.show_all()
@property
def selected_key(self):
return self._selected_key
@staticmethod
def _sort(model, iter1, iter2, _data):
value1 = model[iter1][1]
value2 = model[iter2][1]
if value1 == _('None'):
return -1
if value2 == _('None'):
return 1
if value1 < value2:
return -1
return 1
def _on_response(self, _dialog, _response):
self.destroy()
def _on_row_changed(self, treeview):
selection = treeview.get_selection()
model, iter_ = selection.get_selected()
if iter_ is None:
self._selected_key = None
else:
key_id, key_user = model[iter_][0], model[iter_][1]
if key_id == _('None'):
self._selected_key = None
else:
self._selected_key = key_id, key_user
This diff is collapsed.
# Copyright (C) 2019 Philipp Hörist <philipp AT hoerist.com>
#
# This file is part of the PGP Gajim Plugin.
#
# PGP Gajim Plugin is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# PGP Gajim Plugin is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with PGP Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
import os
import subprocess
import nbxmpp
def prepare_stanza(stanza, plaintext):
delete_nodes(stanza, 'encrypted', nbxmpp.NS_ENCRYPTED)
delete_nodes(stanza, 'body')
stanza.setBody(plaintext)
def delete_nodes(stanza, name, namespace=None):
nodes = stanza.getTags(name, namespace=namespace)
for node in nodes:
stanza.delChild(node)
def find_gpg():
def _search(binary):
if os.name == 'nt':
gpg_cmd = binary + ' -h >nul 2>&1'
else:
gpg_cmd = binary + ' -h >/dev/null 2>&1'
if subprocess.call(gpg_cmd, shell=True):
return False
return True
if _search('gpg2'):
return 'gpg2'
if _search('gpg'):
return 'gpg'
This diff is collapsed.
# Copyright (C) 2019 Philipp Hörist <philipp AT hoerist.com>
#
# This file is part of the PGP Gajim Plugin.
#
# PGP Gajim Plugin is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# PGP Gajim Plugin is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with PGP Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.