[openpgp] Refactor Plugin

- Adapt to nbxmpp now supporting openpgp
This commit is contained in:
Philipp Hörist
2019-02-09 20:38:20 +01:00
parent 0189214c41
commit 2b9780a9f9
10 changed files with 484 additions and 769 deletions

View File

@@ -1,20 +1,18 @@
# Copyright (C) 2018 Philipp Hörist <philipp AT hoerist.com> # Copyright (C) 2019 Philipp Hörist <philipp AT hoerist.com>
# #
# This file is part of Gajim. # This file is part of the OpenPGP Gajim Plugin.
# #
# Gajim is free software; you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only. # by the Free Software Foundation; version 3 only.
# #
# Gajim is distributed in the hope that it will be useful, # OpenPGP Gajim Plugin is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details. # GNU General Public License for more details.
# #
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>. # along with OpenPGP Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
# XEP-0373: OpenPGP for XMPP
import io import io

View File

@@ -1,20 +1,18 @@
# Copyright (C) 2018 Philipp Hörist <philipp AT hoerist.com> # Copyright (C) 2019 Philipp Hörist <philipp AT hoerist.com>
# #
# This file is part of Gajim. # This file is part of the OpenPGP Gajim Plugin.
# #
# Gajim is free software; you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only. # by the Free Software Foundation; version 3 only.
# #
# Gajim is distributed in the hope that it will be useful, # OpenPGP Gajim Plugin is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details. # GNU General Public License for more details.
# #
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>. # along with OpenPGP Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
# XEP-0373: OpenPGP for XMPP
import os import os
import logging import logging
@@ -27,7 +25,6 @@ from gajim.common import app
from openpgp.modules.util import DecryptionFailed from openpgp.modules.util import DecryptionFailed
log = logging.getLogger('gajim.plugin_system.openpgp.pygnupg') log = logging.getLogger('gajim.plugin_system.openpgp.pygnupg')
# gnupg.logger = log
KeyringItem = namedtuple('KeyringItem', 'jid keyid fingerprint') KeyringItem = namedtuple('KeyringItem', 'jid keyid fingerprint')
@@ -38,7 +35,7 @@ class PGPContext(gnupg.GPG):
self, gpgbinary=app.get_gpg_binary(), gnupghome=str(gnupghome)) self, gpgbinary=app.get_gpg_binary(), gnupghome=str(gnupghome))
self._passphrase = 'gajimopenpgppassphrase' self._passphrase = 'gajimopenpgppassphrase'
self._jid = jid self._jid = jid.getBare()
self._own_fingerprint = None self._own_fingerprint = None
def _get_key_params(self, jid, passphrase): def _get_key_params(self, jid, passphrase):
@@ -122,7 +119,7 @@ class PGPContext(gnupg.GPG):
log.error(result.results[0]) log.error(result.results[0])
return return
if not self.validate_key(data, jid): if not self.validate_key(data, str(jid)):
return None return None
key = self.get_key(result.results[0]['fingerprint']) key = self.get_key(result.results[0]['fingerprint'])
return self._make_keyring_item(key[0]) return self._make_keyring_item(key[0])

View File

@@ -1,30 +1,30 @@
# Copyright (C) 2018 Philipp Hörist <philipp AT hoerist.com> # Copyright (C) 2019 Philipp Hörist <philipp AT hoerist.com>
# #
# This file is part of Gajim. # This file is part of the OpenPGP Gajim Plugin.
# #
# Gajim is free software; you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only. # by the Free Software Foundation; version 3 only.
# #
# Gajim is distributed in the hope that it will be useful, # OpenPGP Gajim Plugin is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details. # GNU General Public License for more details.
# #
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>. # along with OpenPGP Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
# XEP-0373: OpenPGP for XMPP
import sqlite3 import sqlite3
import logging import logging
from collections import namedtuple from collections import namedtuple
from nbxmpp import JID
log = logging.getLogger('gajim.plugin_system.openpgp.sql') log = logging.getLogger('gajim.plugin_system.openpgp.sql')
TABLE_LAYOUT = ''' TABLE_LAYOUT = '''
CREATE TABLE contacts ( CREATE TABLE contacts (
jid TEXT, jid JID,
fingerprint TEXT, fingerprint TEXT,
active BOOLEAN, active BOOLEAN,
trust INTEGER, trust INTEGER,
@@ -34,10 +34,25 @@ TABLE_LAYOUT = '''
CREATE UNIQUE INDEX jid_fingerprint ON contacts (jid, fingerprint);''' CREATE UNIQUE INDEX jid_fingerprint ON contacts (jid, fingerprint);'''
def _jid_adapter(jid):
return str(jid)
def _jid_converter(jid):
return JID(jid.decode())
sqlite3.register_adapter(JID, _jid_adapter)
sqlite3.register_converter('JID', _jid_converter)
class Storage: class Storage:
def __init__(self, folder_path): def __init__(self, folder_path):
self._con = sqlite3.connect(str(folder_path / 'contacts.db'), self._con = sqlite3.connect(str(folder_path / 'contacts.db'),
detect_types=sqlite3.PARSE_DECLTYPES) detect_types=sqlite3.PARSE_DECLTYPES)
self._con.row_factory = self._namedtuple_factory self._con.row_factory = self._namedtuple_factory
self._create_database() self._create_database()
self._migrate_database() self._migrate_database()
@@ -72,10 +87,7 @@ class Storage:
pass pass
def load_contacts(self): def load_contacts(self):
sql = 'SELECT * from contacts' return self._con.execute('SELECT * from contacts').fetchall()
rows = self._con.execute(sql).fetchall()
if rows is not None:
return rows
def save_contact(self, db_values): def save_contact(self, db_values):
sql = '''REPLACE INTO sql = '''REPLACE INTO

View File

@@ -1,20 +1,18 @@
# Copyright (C) 2018 Philipp Hörist <philipp AT hoerist.com> # Copyright (C) 2019 Philipp Hörist <philipp AT hoerist.com>
# #
# This file is part of Gajim. # This file is part of the OpenPGP Gajim Plugin.
# #
# Gajim is free software; you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only. # by the Free Software Foundation; version 3 only.
# #
# Gajim is distributed in the hope that it will be useful, # OpenPGP Gajim Plugin is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details. # GNU General Public License for more details.
# #
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>. # along with OpenPGP Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
# XEP-0373: OpenPGP for XMPP
import logging import logging
import time import time
@@ -24,7 +22,7 @@ from gi.repository import Gtk
from gajim.common import app from gajim.common import app
from gajim.common.const import DialogButton, ButtonAction from gajim.common.const import DialogButton, ButtonAction
from gajim.gtk import NewConfirmationDialog from gajim.gtk.dialogs import NewConfirmationDialog
from gajim.plugins.plugins_i18n import _ from gajim.plugins.plugins_i18n import _
from openpgp.modules.util import Trust from openpgp.modules.util import Trust
@@ -49,8 +47,8 @@ TRUST_DATA = {
class KeyDialog(Gtk.Dialog): class KeyDialog(Gtk.Dialog):
def __init__(self, account, jid, transient): def __init__(self, account, jid, transient):
flags = Gtk.DialogFlags.DESTROY_WITH_PARENT super().__init__(title=_('Public Keys for %s') % jid,
super().__init__(_('Public Keys for %s') % jid, None, flags) destroy_with_parent=True)
self.set_transient_for(transient) self.set_transient_for(transient)
self.set_resizable(True) self.set_resizable(True)

View File

@@ -1,20 +1,18 @@
# Copyright (C) 2018 Philipp Hörist <philipp AT hoerist.com> # Copyright (C) 2019 Philipp Hörist <philipp AT hoerist.com>
# #
# This file is part of Gajim. # This file is part of the OpenPGP Gajim Plugin.
# #
# Gajim is free software; you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only. # by the Free Software Foundation; version 3 only.
# #
# Gajim is distributed in the hope that it will be useful, # OpenPGP Gajim Plugin is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details. # GNU General Public License for more details.
# #
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>. # along with OpenPGP Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
# XEP-0373: OpenPGP for XMPP
import logging import logging
import threading import threading
@@ -177,8 +175,8 @@ class NewKeyPage(RequestPage):
error = e error = e
else: else:
self._con.get_module('OpenPGP').get_own_key_details() self._con.get_module('OpenPGP').get_own_key_details()
self._con.get_module('OpenPGP').publish_key() self._con.get_module('OpenPGP').set_public_key()
self._con.get_module('OpenPGP').query_key_list() self._con.get_module('OpenPGP').request_keylist()
GLib.idle_add(self.finished, error) GLib.idle_add(self.finished, error)
def finished(self, error): def finished(self, error):

View File

@@ -0,0 +1,257 @@
# Copyright (C) 2019 Philipp Hörist <philipp AT hoerist.com>
#
# 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 <http://www.gnu.org/licenses/>.
import logging
from openpgp.modules.util import Trust
log = logging.getLogger('gajim.plugin_system.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

View File

@@ -1,40 +1,47 @@
# Copyright (C) 2018 Philipp Hörist <philipp AT hoerist.com> # Copyright (C) 2019 Philipp Hörist <philipp AT hoerist.com>
# #
# This file is part of Gajim. # This file is part of the OpenPGP Gajim Plugin.
# #
# Gajim is free software; you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only. # by the Free Software Foundation; version 3 only.
# #
# Gajim is distributed in the hope that it will be useful, # OpenPGP Gajim Plugin is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details. # GNU General Public License for more details.
# #
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>. # along with OpenPGP Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
# XEP-0373: OpenPGP for XMPP
import time import time
import logging import logging
from pathlib import Path from pathlib import Path
from base64 import b64decode, b64encode
from nbxmpp import Node, isResultNode import nbxmpp
from nbxmpp import Node
from nbxmpp import StanzaMalformed
from nbxmpp.util import is_error_result
from nbxmpp.structs import StanzaHandler
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 app
from gajim.common import configpaths from gajim.common import configpaths
from gajim.common.nec import NetworkEvent 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 import util
from openpgp.modules.util import ENCRYPTION_NAME from openpgp.modules.util import ENCRYPTION_NAME
from openpgp.modules.util import add_additional_data from openpgp.modules.util import NOT_ENCRYPTED_TAGS
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 Key
from openpgp.modules.util import Trust from openpgp.modules.util import add_additional_data
from openpgp.modules.util import DecryptionFailed 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 from openpgp.backend.sql import Storage
from openpgp.backend.pygpg import PGPContext from openpgp.backend.pygpg import PGPContext
@@ -47,252 +54,34 @@ name = ENCRYPTION_NAME
zeroconf = False zeroconf = False
class KeyData: class OpenPGP(BaseModule):
'''
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 _nbxmpp_extends = 'OpenPGP'
def trust(self): _nbxmpp_methods = [
return self._trust 'set_keylist',
'request_keylist',
'set_public_key',
'request_public_key',
'set_secret_key',
'request_secret_key',
]
@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): def __init__(self, con):
self._con = con BaseModule.__init__(self, con)
self._account = con.name
self.handlers = [] self.handlers = [
StanzaHandler(name='message',
callback=self.decrypt_message,
ns=nbxmpp.NS_OPENPGP,
priority=9),
]
self.own_jid = self.get_own_jid(stripped=True) self._register_pubsub_handler(self._keylist_notification_received)
path = Path(configpaths.get('MY_DATA')) / 'openpgp' / self.own_jid 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(): if not path.exists():
path.mkdir(parents=True) path.mkdir(parents=True)
@@ -306,11 +95,6 @@ class OpenPGP:
def secret_key_available(self): def secret_key_available(self):
return self._fingerprint is not None 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): def get_own_key_details(self):
self._fingerprint, self._date = self._pgp.get_own_key_details() self._fingerprint, self._date = self._pgp.get_own_key_details()
return self._fingerprint, self._date return self._fingerprint, self._date
@@ -318,105 +102,77 @@ class OpenPGP:
def generate_key(self): def generate_key(self):
self._pgp.generate_key() self._pgp.generate_key()
def publish_key(self): def set_public_key(self):
log.info('%s => Publish key', self._account) log.info('%s => Publish public key', self._account)
key = self._pgp.export_key(self._fingerprint) key = self._pgp.export_key(self._fingerprint)
self._nbxmpp('OpenPGP').set_public_key(
key, self._fingerprint, self._date)
date = time.strftime( def request_public_key(self, jid, fingerprint):
'%Y-%m-%dT%H:%M:%SZ', time.gmtime(self._date)) log.info('%s => Request public key %s - %s',
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) self._account, fingerprint, jid)
node = '%s:%s' % (NS_OPENPGP_PUBLIC_KEYS, fingerprint) self._nbxmpp('OpenPGP').request_public_key(
self._con.get_module('PubSub').send_pb_retrieve( jid,
jid, node, cb=self._public_key_received, fingerprint=fingerprint) fingerprint,
callback=self._public_key_received,
user_data=fingerprint)
def _public_key_received(self, con, stanza, fingerprint): def _public_key_received(self, result, fingerprint):
if not isResultNode(stanza): if is_error_result(result):
log.error('%s => Public Key not found: %s', log.error('%s => Public Key not found: %s',
self._account, stanza.getError()) self._account, result)
return
pubkey = util.unpack_public_key(stanza, fingerprint)
if pubkey is None:
log.warning('Invalid public key received:\n%s', stanza)
return return
jid = stanza.getFrom().getStripped() imported_key = self._pgp.import_key(result.key, result.jid)
result = self._pgp.import_key(pubkey, jid) if imported_key is not None:
if result is not None: self._contacts.set_public_key(result.jid, fingerprint)
self._contacts.set_public_key(jid, fingerprint)
def query_key_list(self, jid=None): 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(nbxmpp.NS_OPENPGP_PK)
def _keylist_notification_received(self, _con, _stanza, properties):
keylist = []
if not properties.pubsub_event.empty:
keylist = properties.pubsub_event.data
self._process_keylist(keylist, properties.jid)
def request_keylist(self, jid=None):
if jid is None: if jid is None:
jid = self.own_jid jid = self.own_jid
log.info('%s => Fetch keys list %s', self._account, jid) log.info('%s => Fetch keylist %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): self._nbxmpp('OpenPGP').request_keylist(
from_jid = stanza.getFrom() jid,
if from_jid is None: callback=self._keylist_received,
from_jid = self.own_jid user_data=jid)
else:
from_jid = from_jid.getStripped()
if not isResultNode(stanza): def _keylist_received(self, result, jid):
log.error('%s => Keys list query failed: %s', if is_error_result(result):
self._account, stanza.getError()) log.error('%s => Keylist query failed: %s',
if from_jid == self.own_jid and self._fingerprint is not None: self._account, result)
self._publish_key_list() if self.own_jid.bareMatch(jid) and self._fingerprint is not None:
self.set_keylist()
return return
from_jid = stanza.getFrom() log.info('Keylist received from %s', jid)
if from_jid is None: self._process_keylist(result, jid)
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
def _process_keylist(self, keylist, from_jid):
if not keylist: if not keylist:
log.warning('%s => Empty keys list received from %s', log.warning('%s => Empty keylist received from %s',
self._account, from_jid) self._account, from_jid)
self._contacts.process_keylist(self.own_jid, keylist) self._contacts.process_keylist(self.own_jid, keylist)
if from_jid == self.own_jid and self._fingerprint is not None: if self.own_jid.bareMatch(from_jid) and self._fingerprint is not None:
self._publish_key_list() self.set_keylist()
return return
if from_jid == self.own_jid: if self.own_jid.bareMatch(from_jid):
log.info('Received own keys list') log.info('Received own keylist')
for key in keylist: for key in keylist:
log.info(key.fingerprint) log.info(key.fingerprint)
for key in keylist: for key in keylist:
@@ -427,7 +183,7 @@ class OpenPGP:
log.info('Own key not published') log.info('Own key not published')
if self._fingerprint is not None: if self._fingerprint is not None:
keylist.append(Key(self._fingerprint, self._date)) keylist.append(Key(self._fingerprint, self._date))
self._publish_key_list(keylist) self.set_keylist(keylist)
return return
missing_pub_keys = self._contacts.process_keylist(from_jid, keylist) missing_pub_keys = self._contacts.process_keylist(from_jid, keylist)
@@ -436,83 +192,48 @@ class OpenPGP:
log.info(key.fingerprint) log.info(key.fingerprint)
for fingerprint in missing_pub_keys: for fingerprint in missing_pub_keys:
self._query_public_key(from_jid, fingerprint) self.request_public_key(from_jid, fingerprint)
def decrypt_message(self, obj, callback): def decrypt_message(self, _con, stanza, properties):
if obj.encrypted: if not properties.is_openpgp:
# Another Plugin already decrypted the message
return 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: try:
decrypted_payload, fingerprint = self._pgp.decrypt( payload, fingerprint = self._pgp.decrypt(properties.openpgp)
encrypted_payload)
except DecryptionFailed as error: except DecryptionFailed as error:
log.warning(error) log.warning(error)
return return
signcrypt = Node(node=decrypted_payload) signcrypt = Node(node=payload)
signcrypt_jid = signcrypt.getTagAttr('to', 'jid') try:
if self.own_jid != signcrypt_jid: payload, to, timestamp = parse_signcrypt(signcrypt)
log.warning('signcrypt "to" attr %s != %s', except StanzaMalformed as error:
self.own_jid, signcrypt_jid) log.warning('Decryption failed: %s', error)
log.debug(signcrypt) log.warning(payload)
return return
payload = signcrypt.getTag('payload') if not self.own_jid.bareMatch(to):
log.warning('to attr not valid')
log.warning(payload)
return
body = None log.info('Received OpenPGP message from: %s', properties.jid)
if obj.name == 'message-received': prepare_stanza(stanza, payload)
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: properties.encrypted = EncryptionData({'name': ENCRYPTION_NAME,
obj.msgtxt = body 'fingerprint': fingerprint})
add_additional_data(obj.additional_data,
fingerprint)
obj.encrypted = ENCRYPTION_NAME
callback(obj)
def encrypt_message(self, obj, callback): def encrypt_message(self, obj, callback):
keys = self._contacts.get_keys(obj.jid) keys = self._contacts.get_keys(obj.jid)
if not keys: if not keys:
# TODO: this should never happen in theory
log.error('Droping stanza to %s, because we have no key', obj.jid) log.error('Droping stanza to %s, because we have no key', obj.jid)
return return
keys += self._contacts.get_keys(self.own_jid) keys += self._contacts.get_keys(self.own_jid)
keys += [Key(self._fingerprint, None)] keys += [Key(self._fingerprint, None)]
payload = util.create_signcrypt_node(obj) payload = create_signcrypt_node(obj.msg_iq, NOT_ENCRYPTED_TAGS)
encrypted_payload, error = self._pgp.encrypt(payload, keys) encrypted_payload, error = self._pgp.encrypt(payload, keys)
if error: if error:
@@ -523,16 +244,15 @@ class OpenPGP:
jid=obj.jid, jid=obj.jid,
message=obj.message, message=obj.message,
error=error, error=error,
time_=time.time())) time_=time.time(),
session=None))
return return
util.create_openpgp_message(obj, encrypted_payload) create_message_stanza(obj.msg_iq, encrypted_payload, bool(obj.message))
add_additional_data(obj.additional_data, add_additional_data(obj.additional_data,
self._fingerprint) self._fingerprint)
obj.encrypted = ENCRYPTION_NAME obj.encrypted = ENCRYPTION_NAME
self.print_msg_to_log(obj.msg_iq)
callback(obj) callback(obj)
@staticmethod @staticmethod
@@ -550,7 +270,7 @@ class OpenPGP:
return self._contacts.get_keys(jid, only_trusted=only_trusted) return self._contacts.get_keys(jid, only_trusted=only_trusted)
def clear_fingerprints(self): def clear_fingerprints(self):
self._publish_key_list() self.set_keylist()
def cleanup(self): def cleanup(self):
self._storage.cleanup() self._storage.cleanup()

View File

@@ -1,116 +0,0 @@
# Copyright (C) 2018 Philipp Hörist <philipp AT hoerist.com>
#
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
# XEP-0373: OpenPGP for XMPP
import logging
import time
import nbxmpp
from gajim.common import app
from gajim.common.exceptions import StanzaMalformed
from gajim.common.modules.pep import AbstractPEPModule, AbstractPEPData
from gajim.common.modules.date_and_time import parse_datetime
from openpgp.modules import util
from openpgp.modules.util import Key
log = logging.getLogger('gajim.plugin_system.openpgp.pep')
# Module name
name = 'PGPKeylist'
zeroconf = False
class PGPKeylistData(AbstractPEPData):
type_ = 'openpgp-keylist'
class PGPKeylist(AbstractPEPModule):
'''
<item>
<public-keys-list xmlns='urn:xmpp:openpgp:0'>
<pubkey-metadata
v4-fingerprint='1357B01865B2503C18453D208CAC2A9678548E35'
date='2018-03-01T15:26:12Z'
/>
<pubkey-metadata
v4-fingerprint='67819B343B2AB70DED9320872C6464AF2A8E4C02'
date='1953-05-16T12:00:00Z'
/>
</public-keys-list>
</item>
'''
name = 'openpgp-keylist'
namespace = util.NS_OPENPGP_PUBLIC_KEYS
pep_class = PGPKeylistData
store_publish = True
_log = log
def _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'

View File

@@ -1,49 +1,38 @@
# Copyright (C) 2018 Philipp Hörist <philipp AT hoerist.com> # Copyright (C) 2019 Philipp Hörist <philipp AT hoerist.com>
# #
# This file is part of Gajim. # This file is part of the OpenPGP Gajim Plugin.
# #
# Gajim is free software; you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only. # by the Free Software Foundation; version 3 only.
# #
# Gajim is distributed in the hope that it will be useful, # OpenPGP Gajim Plugin is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details. # GNU General Public License for more details.
# #
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>. # along with OpenPGP Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
# XEP-0373: OpenPGP for XMPP
import logging
import random
import time
import string
from enum import IntEnum from enum import IntEnum
from collections import namedtuple from collections import namedtuple
from base64 import b64decode, b64encode
import nbxmpp import nbxmpp
from nbxmpp import Node
from gajim.common.modules.date_and_time import parse_datetime
ENCRYPTION_NAME = 'OpenPGP' 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), NOT_ENCRYPTED_TAGS = [
('store', nbxmpp.NS_MSG_HINTS), ('no-store', nbxmpp.NS_MSG_HINTS),
('no-copy', nbxmpp.NS_MSG_HINTS), ('store', nbxmpp.NS_MSG_HINTS),
('no-permanent-store', nbxmpp.NS_MSG_HINTS), ('no-copy', nbxmpp.NS_MSG_HINTS),
('thread', None)] ('no-permanent-store', nbxmpp.NS_MSG_HINTS),
('origin-id', nbxmpp.NS_SID),
('thread', None)
]
Key = namedtuple('Key', 'fingerprint date') Key = namedtuple('Key', 'fingerprint date')
log = logging.getLogger('gajim.plugin_system.openpgp.util')
class Trust(IntEnum): class Trust(IntEnum):
NOT_TRUSTED = 0 NOT_TRUSTED = 0
@@ -52,159 +41,25 @@ class Trust(IntEnum):
VERIFIED = 3 VERIFIED = 3
def unpack_public_key_list(stanza, from_jid): def prepare_stanza(stanza, payload):
fingerprints = [] delete_nodes(stanza, 'openpgp', nbxmpp.NS_OPENPGP)
delete_nodes(stanza, 'body')
parent = stanza.getTag('pubsub', namespace=nbxmpp.NS_PUBSUB) nodes = [(node.getName(), node.getNamespace()) for node in payload]
if parent is None: for name, namespace in nodes:
parent = stanza.getTag('event', namespace=nbxmpp.NS_PUBSUB_EVENT) delete_nodes(stanza, name, namespace)
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}) for node in payload:
if items is None: stanza.addChild(node=node)
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): def delete_nodes(stanza, name, namespace=None):
pubsub = stanza.getTag('pubsub', namespace=nbxmpp.NS_PUBSUB) attrs = None
if pubsub is None: if namespace is not None:
log.warning('PGP public key has no pubsub node') attrs = {'xmlns': nbxmpp.NS_OPENPGP}
return nodes = stanza.getTags(name, attrs)
node = '%s:%s' % (NS_OPENPGP_PUBLIC_KEYS, fingerprint) for node in nodes:
items = pubsub.getTag('items', attrs={'node': node}) stanza.delChild(node)
if items is None:
log.warning('PGP public key has no items node')
return
item = items.getTags('item')
if not item:
log.warning('PGP public key has no item node')
return
if len(item) > 1:
log.warning('PGP public key has more than one item')
return
pub_key = item[0].getTag('pubkey', namespace=NS_OPENPGP)
if pub_key is None:
log.warning('PGP public key has no pubkey node')
return
data = pub_key.getTag('data')
if data is None:
log.warning('PGP public key has no data node')
return
return b64decode(data.getData().encode('utf8'))
def create_signcrypt_node(obj):
'''
<signcrypt xmlns='urn:xmpp:openpgp:0'>
<to jid='juliet@example.org'/>
<time stamp='2014-07-10T17:06:00+02:00'/>
<rpad>
f0rm1l4n4-mT8y33j!Y%fRSrcd^ZE4Q7VDt1L%WEgR!kv
</rpad>
<payload>
<body xmlns='jabber:client'>
This is a secret message.
</body>
</payload>
</signcrypt>
'''
encrypted_nodes = []
child_nodes = obj.msg_iq.getChildren()
for node in child_nodes:
if (node.name, node.namespace) not in NOT_ENCRYPTED_TAGS:
if not node.namespace:
node.setNamespace(nbxmpp.NS_CLIENT)
encrypted_nodes.append(node)
obj.msg_iq.delChild(node)
signcrypt = Node('signcrypt', attrs={'xmlns': NS_OPENPGP})
signcrypt.addChild('to', attrs={'jid': obj.jid})
timestamp = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
signcrypt.addChild('time', attrs={'stamp': timestamp})
signcrypt.addChild('rpad').addData(get_rpad())
payload = signcrypt.addChild('payload')
for node in encrypted_nodes:
payload.addChild(node=node)
return signcrypt
def get_rpad():
rpad_range = random.randint(30, 50)
return ''.join(
random.choice(string.ascii_letters) for _ in range(rpad_range))
def create_openpgp_message(obj, encrypted_payload):
b64encoded_payload = b64encode(encrypted_payload).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): def add_additional_data(data, fingerprint):

View File

@@ -1,20 +1,18 @@
# Copyright (C) 2018 Philipp Hörist <philipp AT hoerist.com> # Copyright (C) 2019 Philipp Hörist <philipp AT hoerist.com>
# #
# This file is part of Gajim. # This file is part of the OpenPGP Gajim Plugin.
# #
# Gajim is free software; you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only. # by the Free Software Foundation; version 3 only.
# #
# Gajim is distributed in the hope that it will be useful, # OpenPGP Gajim Plugin is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details. # GNU General Public License for more details.
# #
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>. # along with OpenPGP Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
# XEP-0373: OpenPGP for XMPP
import logging import logging
import os import os
@@ -22,19 +20,21 @@ from pathlib import Path
from gi.repository import Gtk from gi.repository import Gtk
from gi.repository import Gdk from gi.repository import Gdk
import nbxmpp
from nbxmpp import JID
from gajim.plugins import GajimPlugin
from gajim.common import app from gajim.common import app
from gajim.common import ged from gajim.common import ged
from gajim.common import configpaths from gajim.common import configpaths
from gajim.common import helpers from gajim.common import helpers
from gajim.common.const import CSSPriority from gajim.common.const import CSSPriority
from gajim.gtk.dialogs import ErrorDialog from gajim.gtk.dialogs import ErrorDialog
from gajim.plugins import GajimPlugin
from gajim.plugins.plugins_i18n import _ from gajim.plugins.plugins_i18n import _
from openpgp.modules.util import NS_NOTIFY
from openpgp.modules.util import ENCRYPTION_NAME from openpgp.modules.util import ENCRYPTION_NAME
from openpgp.modules import pgp_keylist
try: try:
from openpgp.modules import openpgp from openpgp.modules import openpgp
except ImportError as e: except ImportError as e:
@@ -45,8 +45,6 @@ else:
log = logging.getLogger('gajim.plugin_system.openpgp') 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): class OpenPGPPlugin(GajimPlugin):
def init(self): def init(self):
if ERROR_MSG: if ERROR_MSG:
@@ -59,14 +57,12 @@ class OpenPGPPlugin(GajimPlugin):
'signed-in': (ged.PRECORE, self.signed_in), 'signed-in': (ged.PRECORE, self.signed_in),
} }
self.modules = [pgp_keylist, self.modules = [openpgp]
openpgp]
self.encryption_name = ENCRYPTION_NAME self.encryption_name = ENCRYPTION_NAME
self.config_dialog = None self.config_dialog = None
self.gui_extension_points = { self.gui_extension_points = {
'encrypt' + self.encryption_name: (self._encrypt_message, None), 'encrypt' + self.encryption_name: (self._encrypt_message, None),
'decrypt': (self._decrypt_message, None),
'send_message' + self.encryption_name: ( 'send_message' + self.encryption_name: (
self._before_sendmessage, None), self._before_sendmessage, None),
'encryption_dialog' + self.encryption_name: ( 'encryption_dialog' + self.encryption_name: (
@@ -114,8 +110,8 @@ class OpenPGPPlugin(GajimPlugin):
if con.get_module('OpenPGP').secret_key_available: if con.get_module('OpenPGP').secret_key_available:
log.info('%s => Publish keylist and public key after sign in', log.info('%s => Publish keylist and public key after sign in',
account) account)
con.get_module('OpenPGP').query_key_list() con.get_module('OpenPGP').request_keylist()
con.get_module('OpenPGP').publish_key() con.get_module('OpenPGP').set_public_key()
def activate(self): def activate(self):
for account in app.connections: for account in app.connections:
@@ -128,16 +124,17 @@ class OpenPGPPlugin(GajimPlugin):
if con.get_module('OpenPGP').secret_key_available: if con.get_module('OpenPGP').secret_key_available:
log.info('%s => Publish keylist and public key ' log.info('%s => Publish keylist and public key '
'after plugin activation', account) 'after plugin activation', account)
con.get_module('OpenPGP').query_key_list() con.get_module('OpenPGP').request_keylist()
con.get_module('OpenPGP').publish_key() con.get_module('OpenPGP').set_public_key()
def deactivate(self): def deactivate(self):
pass pass
@staticmethod @staticmethod
def _update_caps(account): def _update_caps(account):
if NS_NOTIFY not in app.gajim_optional_features[account]: namespace = nbxmpp.NS_OPENPGP_PK + '+notify'
app.gajim_optional_features[account].append(NS_NOTIFY) if namespace not in app.gajim_optional_features[account]:
app.gajim_optional_features[account].append(namespace)
def activate_encryption(self, chat_control): def activate_encryption(self, chat_control):
account = chat_control.account account = chat_control.account
@@ -147,21 +144,24 @@ class OpenPGPPlugin(GajimPlugin):
keys = app.connections[account].get_module('OpenPGP').get_keys( keys = app.connections[account].get_module('OpenPGP').get_keys(
jid, only_trusted=False) jid, only_trusted=False)
if not keys: if not keys:
con.get_module('OpenPGP').query_key_list(jid) con.get_module('OpenPGP').request_keylist(JID(jid))
ErrorDialog( ErrorDialog(
_('No OpenPGP key'), _('No OpenPGP key'),
_('We didnt receive a OpenPGP key from this contact.')) _('We didnt receive a OpenPGP key from this contact.'))
return return
return True return True
else:
from openpgp.gtk.wizard import KeyWizard
KeyWizard(self, account, chat_control)
def encryption_state(self, chat_control, state): from openpgp.gtk.wizard import KeyWizard
KeyWizard(self, account, chat_control)
return False
@staticmethod
def encryption_state(_chat_control, state):
state['authenticated'] = True state['authenticated'] = True
state['visible'] = True state['visible'] = True
def on_encryption_button_clicked(self, chat_control): @staticmethod
def on_encryption_button_clicked(chat_control):
account = chat_control.account account = chat_control.account
jid = chat_control.contact.jid jid = chat_control.contact.jid
transient = chat_control.parent_win.window transient = chat_control.parent_win.window
@@ -186,12 +186,8 @@ class OpenPGPPlugin(GajimPlugin):
_('There was no trusted and active key found')) _('There was no trusted and active key found'))
chat_control.sendmessage = False chat_control.sendmessage = False
def _encrypt_message(self, con, obj, callback): @staticmethod
def _encrypt_message(con, obj, callback):
if not con.get_module('OpenPGP').secret_key_available: if not con.get_module('OpenPGP').secret_key_available:
return return
con.get_module('OpenPGP').encrypt_message(obj, callback) 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)