[openpgp] Refactor Plugin
- Adapt to nbxmpp now supporting openpgp
This commit is contained in:
@@ -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
|
||||
# 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
|
||||
# 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
|
||||
# along with OpenPGP Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import io
|
||||
|
||||
@@ -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
|
||||
# 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
|
||||
# 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
|
||||
# along with OpenPGP Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import logging
|
||||
@@ -27,7 +25,6 @@ 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')
|
||||
|
||||
@@ -38,7 +35,7 @@ class PGPContext(gnupg.GPG):
|
||||
self, gpgbinary=app.get_gpg_binary(), gnupghome=str(gnupghome))
|
||||
|
||||
self._passphrase = 'gajimopenpgppassphrase'
|
||||
self._jid = jid
|
||||
self._jid = jid.getBare()
|
||||
self._own_fingerprint = None
|
||||
|
||||
def _get_key_params(self, jid, passphrase):
|
||||
@@ -122,7 +119,7 @@ class PGPContext(gnupg.GPG):
|
||||
log.error(result.results[0])
|
||||
return
|
||||
|
||||
if not self.validate_key(data, jid):
|
||||
if not self.validate_key(data, str(jid)):
|
||||
return None
|
||||
key = self.get_key(result.results[0]['fingerprint'])
|
||||
return self._make_keyring_item(key[0])
|
||||
|
||||
@@ -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
|
||||
# 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
|
||||
# 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
|
||||
# along with OpenPGP Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import sqlite3
|
||||
import logging
|
||||
from collections import namedtuple
|
||||
|
||||
from nbxmpp import JID
|
||||
|
||||
log = logging.getLogger('gajim.plugin_system.openpgp.sql')
|
||||
|
||||
TABLE_LAYOUT = '''
|
||||
CREATE TABLE contacts (
|
||||
jid TEXT,
|
||||
jid JID,
|
||||
fingerprint TEXT,
|
||||
active BOOLEAN,
|
||||
trust INTEGER,
|
||||
@@ -34,10 +34,25 @@ TABLE_LAYOUT = '''
|
||||
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:
|
||||
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()
|
||||
@@ -72,10 +87,7 @@ class Storage:
|
||||
pass
|
||||
|
||||
def load_contacts(self):
|
||||
sql = 'SELECT * from contacts'
|
||||
rows = self._con.execute(sql).fetchall()
|
||||
if rows is not None:
|
||||
return rows
|
||||
return self._con.execute('SELECT * from contacts').fetchall()
|
||||
|
||||
def save_contact(self, db_values):
|
||||
sql = '''REPLACE INTO
|
||||
|
||||
@@ -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
|
||||
# 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
|
||||
# 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
|
||||
# along with OpenPGP Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import logging
|
||||
import time
|
||||
@@ -24,7 +22,7 @@ from gi.repository import Gtk
|
||||
from gajim.common import app
|
||||
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 openpgp.modules.util import Trust
|
||||
@@ -49,8 +47,8 @@ TRUST_DATA = {
|
||||
|
||||
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)
|
||||
super().__init__(title=_('Public Keys for %s') % jid,
|
||||
destroy_with_parent=True)
|
||||
|
||||
self.set_transient_for(transient)
|
||||
self.set_resizable(True)
|
||||
|
||||
@@ -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
|
||||
# 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
|
||||
# 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
|
||||
# along with OpenPGP Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import logging
|
||||
import threading
|
||||
@@ -177,8 +175,8 @@ class NewKeyPage(RequestPage):
|
||||
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()
|
||||
self._con.get_module('OpenPGP').set_public_key()
|
||||
self._con.get_module('OpenPGP').request_keylist()
|
||||
GLib.idle_add(self.finished, error)
|
||||
|
||||
def finished(self, error):
|
||||
|
||||
257
openpgp/modules/key_store.py
Normal file
257
openpgp/modules/key_store.py
Normal 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
|
||||
@@ -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
|
||||
# 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
|
||||
# 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.
|
||||
#
|
||||
# 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
|
||||
# along with OpenPGP Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import time
|
||||
import logging
|
||||
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 configpaths
|
||||
from gajim.common.nec import NetworkEvent
|
||||
from gajim.common.const import EncryptionData
|
||||
from gajim.common.modules.base import BaseModule
|
||||
from gajim.common.modules.util import event_node
|
||||
|
||||
from openpgp.modules 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 NOT_ENCRYPTED_TAGS
|
||||
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 prepare_stanza
|
||||
from openpgp.modules.key_store import PGPContacts
|
||||
from openpgp.backend.sql import Storage
|
||||
from openpgp.backend.pygpg import PGPContext
|
||||
|
||||
@@ -47,252 +54,34 @@ 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
|
||||
class OpenPGP(BaseModule):
|
||||
|
||||
@property
|
||||
def trust(self):
|
||||
return self._trust
|
||||
_nbxmpp_extends = 'OpenPGP'
|
||||
_nbxmpp_methods = [
|
||||
'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):
|
||||
self._con = con
|
||||
self._account = con.name
|
||||
BaseModule.__init__(self, con)
|
||||
|
||||
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():
|
||||
path.mkdir(parents=True)
|
||||
|
||||
@@ -306,11 +95,6 @@ class OpenPGP:
|
||||
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
|
||||
@@ -318,105 +102,77 @@ class OpenPGP:
|
||||
def generate_key(self):
|
||||
self._pgp.generate_key()
|
||||
|
||||
def publish_key(self):
|
||||
log.info('%s => Publish key', self._account)
|
||||
def set_public_key(self):
|
||||
log.info('%s => Publish public key', self._account)
|
||||
key = self._pgp.export_key(self._fingerprint)
|
||||
self._nbxmpp('OpenPGP').set_public_key(
|
||||
key, self._fingerprint, self._date)
|
||||
|
||||
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',
|
||||
def request_public_key(self, jid, fingerprint):
|
||||
log.info('%s => Request 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)
|
||||
self._nbxmpp('OpenPGP').request_public_key(
|
||||
jid,
|
||||
fingerprint,
|
||||
callback=self._public_key_received,
|
||||
user_data=fingerprint)
|
||||
|
||||
def _public_key_received(self, con, stanza, fingerprint):
|
||||
if not isResultNode(stanza):
|
||||
def _public_key_received(self, result, fingerprint):
|
||||
if is_error_result(result):
|
||||
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)
|
||||
self._account, result)
|
||||
return
|
||||
|
||||
jid = stanza.getFrom().getStripped()
|
||||
result = self._pgp.import_key(pubkey, jid)
|
||||
if result is not None:
|
||||
self._contacts.set_public_key(jid, fingerprint)
|
||||
imported_key = self._pgp.import_key(result.key, result.jid)
|
||||
if imported_key is not None:
|
||||
self._contacts.set_public_key(result.jid, fingerprint)
|
||||
|
||||
def 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:
|
||||
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)
|
||||
log.info('%s => Fetch keylist %s', self._account, jid)
|
||||
|
||||
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()
|
||||
self._nbxmpp('OpenPGP').request_keylist(
|
||||
jid,
|
||||
callback=self._keylist_received,
|
||||
user_data=jid)
|
||||
|
||||
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()
|
||||
def _keylist_received(self, result, jid):
|
||||
if is_error_result(result):
|
||||
log.error('%s => Keylist query failed: %s',
|
||||
self._account, result)
|
||||
if self.own_jid.bareMatch(jid) and self._fingerprint is not None:
|
||||
self.set_keylist()
|
||||
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
|
||||
log.info('Keylist received from %s', jid)
|
||||
self._process_keylist(result, jid)
|
||||
|
||||
def _process_keylist(self, keylist, from_jid):
|
||||
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._contacts.process_keylist(self.own_jid, keylist)
|
||||
if from_jid == self.own_jid and self._fingerprint is not None:
|
||||
self._publish_key_list()
|
||||
if self.own_jid.bareMatch(from_jid) and self._fingerprint is not None:
|
||||
self.set_keylist()
|
||||
return
|
||||
|
||||
if from_jid == self.own_jid:
|
||||
log.info('Received own keys list')
|
||||
if self.own_jid.bareMatch(from_jid):
|
||||
log.info('Received own keylist')
|
||||
for key in keylist:
|
||||
log.info(key.fingerprint)
|
||||
for key in keylist:
|
||||
@@ -427,7 +183,7 @@ class OpenPGP:
|
||||
log.info('Own key not published')
|
||||
if self._fingerprint is not None:
|
||||
keylist.append(Key(self._fingerprint, self._date))
|
||||
self._publish_key_list(keylist)
|
||||
self.set_keylist(keylist)
|
||||
return
|
||||
|
||||
missing_pub_keys = self._contacts.process_keylist(from_jid, keylist)
|
||||
@@ -436,83 +192,48 @@ class OpenPGP:
|
||||
log.info(key.fingerprint)
|
||||
|
||||
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):
|
||||
if obj.encrypted:
|
||||
# Another Plugin already decrypted the message
|
||||
def decrypt_message(self, _con, stanza, properties):
|
||||
if not properties.is_openpgp:
|
||||
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)
|
||||
payload, fingerprint = self._pgp.decrypt(properties.openpgp)
|
||||
except DecryptionFailed as error:
|
||||
log.warning(error)
|
||||
return
|
||||
|
||||
signcrypt = Node(node=decrypted_payload)
|
||||
signcrypt = Node(node=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)
|
||||
try:
|
||||
payload, to, timestamp = parse_signcrypt(signcrypt)
|
||||
except StanzaMalformed as error:
|
||||
log.warning('Decryption failed: %s', error)
|
||||
log.warning(payload)
|
||||
return
|
||||
|
||||
payload = signcrypt.getTag('payload')
|
||||
if not self.own_jid.bareMatch(to):
|
||||
log.warning('to attr not valid')
|
||||
log.warning(payload)
|
||||
return
|
||||
|
||||
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)
|
||||
log.info('Received OpenPGP message from: %s', properties.jid)
|
||||
prepare_stanza(stanza, payload)
|
||||
|
||||
if body:
|
||||
obj.msgtxt = body
|
||||
|
||||
add_additional_data(obj.additional_data,
|
||||
fingerprint)
|
||||
|
||||
obj.encrypted = ENCRYPTION_NAME
|
||||
callback(obj)
|
||||
properties.encrypted = EncryptionData({'name': ENCRYPTION_NAME,
|
||||
'fingerprint': fingerprint})
|
||||
|
||||
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)
|
||||
payload = create_signcrypt_node(obj.msg_iq, NOT_ENCRYPTED_TAGS)
|
||||
|
||||
encrypted_payload, error = self._pgp.encrypt(payload, keys)
|
||||
if error:
|
||||
@@ -523,16 +244,15 @@ class OpenPGP:
|
||||
jid=obj.jid,
|
||||
message=obj.message,
|
||||
error=error,
|
||||
time_=time.time()))
|
||||
time_=time.time(),
|
||||
session=None))
|
||||
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,
|
||||
self._fingerprint)
|
||||
|
||||
obj.encrypted = ENCRYPTION_NAME
|
||||
self.print_msg_to_log(obj.msg_iq)
|
||||
callback(obj)
|
||||
|
||||
@staticmethod
|
||||
@@ -550,7 +270,7 @@ class OpenPGP:
|
||||
return self._contacts.get_keys(jid, only_trusted=only_trusted)
|
||||
|
||||
def clear_fingerprints(self):
|
||||
self._publish_key_list()
|
||||
self.set_keylist()
|
||||
|
||||
def cleanup(self):
|
||||
self._storage.cleanup()
|
||||
|
||||
@@ -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'
|
||||
@@ -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
|
||||
# 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
|
||||
# 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/>.
|
||||
# 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 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)]
|
||||
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),
|
||||
('origin-id', nbxmpp.NS_SID),
|
||||
('thread', None)
|
||||
]
|
||||
|
||||
Key = namedtuple('Key', 'fingerprint date')
|
||||
|
||||
log = logging.getLogger('gajim.plugin_system.openpgp.util')
|
||||
|
||||
|
||||
class Trust(IntEnum):
|
||||
NOT_TRUSTED = 0
|
||||
@@ -52,159 +41,25 @@ class Trust(IntEnum):
|
||||
VERIFIED = 3
|
||||
|
||||
|
||||
def unpack_public_key_list(stanza, from_jid):
|
||||
fingerprints = []
|
||||
def prepare_stanza(stanza, payload):
|
||||
delete_nodes(stanza, 'openpgp', nbxmpp.NS_OPENPGP)
|
||||
delete_nodes(stanza, 'body')
|
||||
|
||||
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
|
||||
nodes = [(node.getName(), node.getNamespace()) for node in payload]
|
||||
for name, namespace in nodes:
|
||||
delete_nodes(stanza, name, namespace)
|
||||
|
||||
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
|
||||
for node in payload:
|
||||
stanza.addChild(node=node)
|
||||
|
||||
|
||||
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):
|
||||
'''
|
||||
<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 delete_nodes(stanza, name, namespace=None):
|
||||
attrs = None
|
||||
if namespace is not None:
|
||||
attrs = {'xmlns': nbxmpp.NS_OPENPGP}
|
||||
nodes = stanza.getTags(name, attrs)
|
||||
for node in nodes:
|
||||
stanza.delChild(node)
|
||||
|
||||
|
||||
def add_additional_data(data, fingerprint):
|
||||
|
||||
@@ -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
|
||||
# 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
|
||||
# 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
|
||||
# along with OpenPGP Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import logging
|
||||
import os
|
||||
@@ -22,19 +20,21 @@ from pathlib import Path
|
||||
|
||||
from gi.repository import Gtk
|
||||
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 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 import GajimPlugin
|
||||
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:
|
||||
@@ -45,8 +45,6 @@ else:
|
||||
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:
|
||||
@@ -59,14 +57,12 @@ class OpenPGPPlugin(GajimPlugin):
|
||||
'signed-in': (ged.PRECORE, self.signed_in),
|
||||
}
|
||||
|
||||
self.modules = [pgp_keylist,
|
||||
openpgp]
|
||||
self.modules = [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: (
|
||||
@@ -114,8 +110,8 @@ class OpenPGPPlugin(GajimPlugin):
|
||||
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()
|
||||
con.get_module('OpenPGP').request_keylist()
|
||||
con.get_module('OpenPGP').set_public_key()
|
||||
|
||||
def activate(self):
|
||||
for account in app.connections:
|
||||
@@ -128,16 +124,17 @@ class OpenPGPPlugin(GajimPlugin):
|
||||
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()
|
||||
con.get_module('OpenPGP').request_keylist()
|
||||
con.get_module('OpenPGP').set_public_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)
|
||||
namespace = nbxmpp.NS_OPENPGP_PK + '+notify'
|
||||
if namespace not in app.gajim_optional_features[account]:
|
||||
app.gajim_optional_features[account].append(namespace)
|
||||
|
||||
def activate_encryption(self, chat_control):
|
||||
account = chat_control.account
|
||||
@@ -147,21 +144,24 @@ class OpenPGPPlugin(GajimPlugin):
|
||||
keys = app.connections[account].get_module('OpenPGP').get_keys(
|
||||
jid, only_trusted=False)
|
||||
if not keys:
|
||||
con.get_module('OpenPGP').query_key_list(jid)
|
||||
con.get_module('OpenPGP').request_keylist(JID(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):
|
||||
from openpgp.gtk.wizard import KeyWizard
|
||||
KeyWizard(self, account, chat_control)
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def encryption_state(_chat_control, state):
|
||||
state['authenticated'] = True
|
||||
state['visible'] = True
|
||||
|
||||
def on_encryption_button_clicked(self, chat_control):
|
||||
@staticmethod
|
||||
def on_encryption_button_clicked(chat_control):
|
||||
account = chat_control.account
|
||||
jid = chat_control.contact.jid
|
||||
transient = chat_control.parent_win.window
|
||||
@@ -186,12 +186,8 @@ class OpenPGPPlugin(GajimPlugin):
|
||||
_('There was no trusted and active key found'))
|
||||
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:
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user