[openpgp] Inital commit
This commit is contained in:
1
openpgp/__init__.py
Normal file
1
openpgp/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .pgpplugin import OpenPGPPlugin
|
||||
0
openpgp/backend/__init__.py
Normal file
0
openpgp/backend/__init__.py
Normal file
196
openpgp/backend/gpgme.py
Normal file
196
openpgp/backend/gpgme.py
Normal file
@@ -0,0 +1,196 @@
|
||||
# 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 io
|
||||
from collections import namedtuple
|
||||
import logging
|
||||
|
||||
import gpg
|
||||
|
||||
from gajim.common import app
|
||||
|
||||
KeyringItem = namedtuple('KeyringItem',
|
||||
'type keyid userid fingerprint')
|
||||
|
||||
log = logging.getLogger('gajim.plugin_system.openpgp.pgpme')
|
||||
|
||||
|
||||
class PGPContext():
|
||||
def __init__(self, jid, gnuhome):
|
||||
self.context = gpg.Context(home_dir=str(gnuhome))
|
||||
# self.create_new_key()
|
||||
# self.get_key_by_name()
|
||||
# self.get_key_by_fingerprint()
|
||||
self.export_public_key()
|
||||
|
||||
def create_new_key(self):
|
||||
parms = """<GnupgKeyParms format="internal">
|
||||
Key-Type: RSA
|
||||
Key-Length: 2048
|
||||
Subkey-Type: RSA
|
||||
Subkey-Length: 2048
|
||||
Name-Real: Joe Tester
|
||||
Name-Comment: with stupid passphrase
|
||||
Name-Email: test@example.org
|
||||
Passphrase: Crypt0R0cks
|
||||
Expire-Date: 2020-12-31
|
||||
</GnupgKeyParms>
|
||||
"""
|
||||
|
||||
with self.context as c:
|
||||
c.set_engine_info(gpg.constants.PROTOCOL_OpenPGP, None, app.gajimpaths['MY_DATA'])
|
||||
c.set_progress_cb(gpg.callbacks.progress_stdout)
|
||||
c.op_genkey(parms, None, None)
|
||||
print("Generated key with fingerprint {0}.".format(
|
||||
c.op_genkey_result().fpr))
|
||||
|
||||
def get_all_keys(self):
|
||||
c = gpg.Context()
|
||||
for key in c.keylist():
|
||||
user = key.uids[0]
|
||||
print("Keys for %s (%s):" % (user.name, user.email))
|
||||
for subkey in key.subkeys:
|
||||
features = []
|
||||
if subkey.can_authenticate:
|
||||
features.append('auth')
|
||||
if subkey.can_certify:
|
||||
features.append('cert')
|
||||
if subkey.can_encrypt:
|
||||
features.append('encrypt')
|
||||
if subkey.can_sign:
|
||||
features.append('sign')
|
||||
print(' %s %s' % (subkey.fpr, ','.join(features)))
|
||||
|
||||
def get_key_by_name(self):
|
||||
c = gpg.Context()
|
||||
for key in c.keylist('john'):
|
||||
print(key.subkeys[0].fpr)
|
||||
|
||||
def get_key_by_fingerprint(self):
|
||||
c = gpg.Context()
|
||||
fingerprint = 'key fingerprint to search for'
|
||||
try:
|
||||
key = c.get_key(fingerprint)
|
||||
print('%s (%s)' % (key.uids[0].name, key.uids[0].email))
|
||||
except gpg.errors.KeyNotFound:
|
||||
print("No key for fingerprint '%s'." % fingerprint)
|
||||
|
||||
def get_secret_key(self):
|
||||
'''
|
||||
Key(can_authenticate=1,
|
||||
can_certify=1,
|
||||
can_encrypt=1,
|
||||
can_sign=1,
|
||||
chain_id=None,
|
||||
disabled=0,
|
||||
expired=0,
|
||||
fpr='7ECE1F88BAFCA37F168E1556A4DBDD1BA55FE3CE',
|
||||
invalid=0,
|
||||
is_qualified=0,
|
||||
issuer_name=None,
|
||||
issuer_serial=None,
|
||||
keylist_mode=1,
|
||||
last_update=0,
|
||||
origin=0,
|
||||
owner_trust=5,
|
||||
protocol=0,
|
||||
revoked=0,
|
||||
secret=1,
|
||||
subkeys=[
|
||||
SubKey(can_authenticate=1,
|
||||
can_certify=1,
|
||||
can_encrypt=1,
|
||||
can_sign=1,
|
||||
card_number=None
|
||||
curve=None,
|
||||
disabled=0,
|
||||
expired=0,
|
||||
expires=0,
|
||||
fpr='7ECE1F88BAFCA37F168E1556A4DBDD1BA55FE3CE',
|
||||
invalid=0,
|
||||
is_cardkey=0,
|
||||
is_de_vs=1,
|
||||
is_qualified=0,
|
||||
keygrip='15BECB77301E4810ABB9CA6A9391158E575DABEC',
|
||||
keyid='A4DBDD1BA55FE3CE',
|
||||
length=2048,
|
||||
pubkey_algo=1,
|
||||
revoked=0,
|
||||
secret=1,
|
||||
timestamp=1525006759)],
|
||||
uids=[
|
||||
UID(address=None,
|
||||
comment='',
|
||||
email='',
|
||||
invalid=0,
|
||||
last_update=0,
|
||||
name='xmpp:philw@jabber.at',
|
||||
origin=0,
|
||||
revoked=0,
|
||||
signatures=[],
|
||||
tofu=[],
|
||||
uid='xmpp:philw@jabber.at',
|
||||
validity=5)])
|
||||
'''
|
||||
for key in self.context.keylist(secret=True):
|
||||
break
|
||||
return key.fpr, key.fpr[-16:]
|
||||
|
||||
def get_keys(self, secret=False):
|
||||
keys = []
|
||||
for key in self.context.keylist():
|
||||
for uid in key.uids:
|
||||
if uid.uid.startswith('xmpp:'):
|
||||
keys.append((key, uid.uid[5:]))
|
||||
break
|
||||
return keys
|
||||
|
||||
def export_public_key(self):
|
||||
# print(dir(self.context))
|
||||
result = self.context.key_export_minimal()
|
||||
print(result)
|
||||
|
||||
def encrypt_decrypt_files(self):
|
||||
c = gpg.Context()
|
||||
recipient = c.get_key("fingerprint of recipient's key")
|
||||
|
||||
# Encrypt
|
||||
with open('foo.txt', 'r') as input_file:
|
||||
with open('foo.txt.gpg', 'wb') as output_file:
|
||||
c.encrypt([recipient], 0, input_file, output_file)
|
||||
|
||||
# Decrypt
|
||||
with open('foo.txt.gpg', 'rb') as input_file:
|
||||
with open('foo2.txt', 'w') as output_file:
|
||||
c.decrypt(input_file, output_file)
|
||||
|
||||
def encrypt(self):
|
||||
c = gpg.Context()
|
||||
recipient = c.get_key("fingerprint of recipient's key")
|
||||
|
||||
plaintext_string = u'plain text data'
|
||||
plaintext_bytes = io.BytesIO(plaintext_string.encode('utf8'))
|
||||
encrypted_bytes = io.BytesIO()
|
||||
c.encrypt([recipient], 0, plaintext_bytes, encrypted_bytes)
|
||||
|
||||
def decrypt(self):
|
||||
c = gpg.Context()
|
||||
decrypted_bytes = io.BytesIO()
|
||||
c.decrypt(encrypted_bytes, decrypted_bytes)
|
||||
decrypted_string = decrypted_bytes.getvalue().decode('utf8')
|
||||
183
openpgp/backend/pygpg.py
Normal file
183
openpgp/backend/pygpg.py
Normal file
@@ -0,0 +1,183 @@
|
||||
# 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 os
|
||||
import logging
|
||||
from collections import namedtuple
|
||||
|
||||
import gnupg
|
||||
|
||||
from gajim.common import app
|
||||
|
||||
from openpgp.modules.util import DecryptionFailed
|
||||
|
||||
log = logging.getLogger('gajim.plugin_system.openpgp.pygnupg')
|
||||
# gnupg.logger = log
|
||||
|
||||
KeyringItem = namedtuple('KeyringItem', 'jid keyid fingerprint')
|
||||
|
||||
|
||||
class PGPContext(gnupg.GPG):
|
||||
def __init__(self, jid, gnupghome):
|
||||
gnupg.GPG.__init__(
|
||||
self, gpgbinary=app.get_gpg_binary(), gnupghome=str(gnupghome))
|
||||
|
||||
self._passphrase = 'gajimopenpgppassphrase'
|
||||
self._jid = jid
|
||||
self._own_fingerprint = None
|
||||
|
||||
def _get_key_params(self, jid, passphrase):
|
||||
'''
|
||||
Generate --gen-key input
|
||||
'''
|
||||
|
||||
params = {
|
||||
'Key-Type': 'RSA',
|
||||
'Key-Length': 2048,
|
||||
'Name-Real': 'xmpp:%s' % jid,
|
||||
'Passphrase': passphrase,
|
||||
}
|
||||
|
||||
out = "Key-Type: %s\n" % params.pop('Key-Type')
|
||||
for key, val in list(params.items()):
|
||||
out += "%s: %s\n" % (key, val)
|
||||
out += "%commit\n"
|
||||
return out
|
||||
|
||||
def generate_key(self):
|
||||
super().gen_key(self._get_key_params(self._jid, self._passphrase))
|
||||
|
||||
def encrypt(self, payload, keys):
|
||||
recipients = [key.fingerprint for key in keys]
|
||||
log.info('encrypt to:')
|
||||
for fingerprint in recipients:
|
||||
log.info(fingerprint)
|
||||
|
||||
result = super().encrypt(str(payload).encode('utf8'),
|
||||
recipients,
|
||||
sign=self._own_fingerprint,
|
||||
always_trust=True,
|
||||
passphrase=self._passphrase)
|
||||
|
||||
if result.ok:
|
||||
error = ''
|
||||
else:
|
||||
error = result.status
|
||||
|
||||
return str(result), error
|
||||
|
||||
def decrypt(self, payload):
|
||||
result = super().decrypt(payload,
|
||||
always_trust=True,
|
||||
passphrase=self._passphrase)
|
||||
if not result.ok:
|
||||
raise DecryptionFailed(result.status)
|
||||
|
||||
return result.data.decode('utf8')
|
||||
|
||||
def get_key(self, fingerprint):
|
||||
return super().list_keys(keys=[fingerprint])
|
||||
|
||||
def get_keys(self, secret=False):
|
||||
result = super().list_keys(secret=secret)
|
||||
keys = []
|
||||
for key in result:
|
||||
item = self._make_keyring_item(key)
|
||||
if item is None:
|
||||
continue
|
||||
keys.append(self._make_keyring_item(key))
|
||||
return keys
|
||||
|
||||
@staticmethod
|
||||
def _make_keyring_item(key):
|
||||
userid = key['uids'][0]
|
||||
if not userid.startswith('xmpp:'):
|
||||
log.warning('Incorrect userid: %s found for key, '
|
||||
'key will be ignored', userid)
|
||||
return
|
||||
jid = userid[5:]
|
||||
return KeyringItem(jid, key['keyid'], key['fingerprint'])
|
||||
|
||||
def import_key(self, data, jid):
|
||||
log.info('Import key from %s', jid)
|
||||
result = super().import_keys(data)
|
||||
if not result:
|
||||
log.error('Could not import key')
|
||||
log.error(result.results[0])
|
||||
return
|
||||
|
||||
if not self.validate_key(data, jid):
|
||||
return None
|
||||
key = self.get_key(result.results[0]['fingerprint'])
|
||||
return self._make_keyring_item(key[0])
|
||||
|
||||
def validate_key(self, public_key, jid):
|
||||
import tempfile
|
||||
temppath = os.path.join(tempfile.gettempdir(), 'temp_pubkey')
|
||||
with open(temppath, 'wb') as tempfile:
|
||||
tempfile.write(public_key)
|
||||
|
||||
result = self.scan_keys(temppath)
|
||||
if result:
|
||||
for uid in result.uids:
|
||||
if uid.startswith('xmpp:'):
|
||||
if uid[5:] == jid:
|
||||
key_found = True
|
||||
else:
|
||||
log.warning('Found wrong userid in key: %s != %s',
|
||||
uid[5:], jid)
|
||||
log.debug(result)
|
||||
os.remove(temppath)
|
||||
return False
|
||||
|
||||
if not key_found:
|
||||
log.warning('No valid userid found in key')
|
||||
log.debug(result)
|
||||
os.remove(temppath)
|
||||
return False
|
||||
|
||||
log.info('Key validation succesful')
|
||||
os.remove(temppath)
|
||||
return True
|
||||
|
||||
log.warning('Invalid key data: %s')
|
||||
log.debug(result)
|
||||
os.remove(temppath)
|
||||
return False
|
||||
|
||||
def get_own_key_details(self):
|
||||
result = super().list_keys(secret=True)
|
||||
if not result:
|
||||
return None, None
|
||||
|
||||
if len(result) > 1:
|
||||
log.error('More than one secret key found')
|
||||
return None, None
|
||||
|
||||
self._own_fingerprint = result[0]['fingerprint']
|
||||
return self._own_fingerprint, int(result[0]['date'])
|
||||
|
||||
def export_key(self, fingerprint):
|
||||
key = super().export_keys(
|
||||
fingerprint, secret=False, armor=False, minimal=False,
|
||||
passphrase=self._passphrase)
|
||||
return key
|
||||
|
||||
def delete_key(self, fingerprint):
|
||||
log.info('Delete Key: %s', fingerprint)
|
||||
super().delete_keys(fingerprint, passphrase=self._passphrase)
|
||||
102
openpgp/backend/sql.py
Normal file
102
openpgp/backend/sql.py
Normal file
@@ -0,0 +1,102 @@
|
||||
# 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 sqlite3
|
||||
import logging
|
||||
from collections import namedtuple
|
||||
|
||||
log = logging.getLogger('gajim.plugin_system.openpgp.sql')
|
||||
|
||||
TABLE_LAYOUT = '''
|
||||
CREATE TABLE contacts (
|
||||
jid TEXT,
|
||||
fingerprint TEXT,
|
||||
active BOOLEAN,
|
||||
trust INTEGER,
|
||||
timestamp INTEGER,
|
||||
comment TEXT
|
||||
);
|
||||
CREATE UNIQUE INDEX jid_fingerprint ON contacts (jid, fingerprint);'''
|
||||
|
||||
|
||||
class Storage:
|
||||
def __init__(self, folder_path):
|
||||
self._con = sqlite3.connect(folder_path / 'contacts.db',
|
||||
detect_types=sqlite3.PARSE_DECLTYPES)
|
||||
self._con.row_factory = self._namedtuple_factory
|
||||
self._create_database()
|
||||
self._migrate_database()
|
||||
self._con.execute("PRAGMA synchronous=FULL;")
|
||||
self._con.commit()
|
||||
|
||||
@staticmethod
|
||||
def _namedtuple_factory(cursor, row):
|
||||
fields = [col[0] for col in cursor.description]
|
||||
Row = namedtuple("Row", fields)
|
||||
named_row = Row(*row)
|
||||
return named_row
|
||||
|
||||
def _user_version(self):
|
||||
return self._con.execute('PRAGMA user_version').fetchone()[0]
|
||||
|
||||
def _create_database(self):
|
||||
if not self._user_version():
|
||||
log.info('Create contacts.db')
|
||||
self._execute_query(TABLE_LAYOUT)
|
||||
|
||||
def _execute_query(self, query):
|
||||
transaction = """
|
||||
BEGIN TRANSACTION;
|
||||
%s
|
||||
PRAGMA user_version=1;
|
||||
END TRANSACTION;
|
||||
""" % (query)
|
||||
self._con.executescript(transaction)
|
||||
|
||||
def _migrate_database(self):
|
||||
pass
|
||||
|
||||
def load_contacts(self):
|
||||
sql = 'SELECT * from contacts'
|
||||
rows = self._con.execute(sql).fetchall()
|
||||
if rows is not None:
|
||||
return rows
|
||||
|
||||
def save_contact(self, db_values):
|
||||
sql = '''REPLACE INTO
|
||||
contacts(jid, fingerprint, active, trust, timestamp, comment)
|
||||
VALUES(?, ?, ?, ?, ?, ?)'''
|
||||
for values in db_values:
|
||||
log.info('Store key: %s', values)
|
||||
self._con.execute(sql, values)
|
||||
self._con.commit()
|
||||
|
||||
def set_trust(self, jid, fingerprint, trust):
|
||||
sql = 'UPDATE contacts SET trust = ? WHERE jid = ? AND fingerprint = ?'
|
||||
log.info('Set Trust: %s %s %s', trust, jid, fingerprint)
|
||||
self._con.execute(sql, (trust, jid, fingerprint))
|
||||
self._con.commit()
|
||||
|
||||
def delete_key(self, jid, fingerprint):
|
||||
sql = 'DELETE from contacts WHERE jid = ? AND fingerprint = ?'
|
||||
log.info('Delete Key: %s %s', jid, fingerprint)
|
||||
self._con.execute(sql, (jid, fingerprint))
|
||||
self._con.commit()
|
||||
|
||||
def cleanup(self):
|
||||
self._con.close()
|
||||
0
openpgp/gtk/__init__.py
Normal file
0
openpgp/gtk/__init__.py
Normal file
263
openpgp/gtk/key.py
Normal file
263
openpgp/gtk/key.py
Normal file
@@ -0,0 +1,263 @@
|
||||
# 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
|
||||
|
||||
from gi.repository import Gtk
|
||||
|
||||
from gajim.common import app
|
||||
from gajim.common.const import DialogButton, ButtonAction
|
||||
|
||||
from gajim.gtk import NewConfirmationDialog
|
||||
|
||||
from openpgp.modules.util import Trust
|
||||
|
||||
log = logging.getLogger('gajim.plugin_system.openpgp.keydialog')
|
||||
|
||||
TRUST_DATA = {
|
||||
Trust.NOT_TRUSTED: ('dialog-error-symbolic',
|
||||
_('Not Trusted'),
|
||||
'error-color'),
|
||||
Trust.UNKNOWN: ('security-low-symbolic',
|
||||
_('Not Decided'),
|
||||
'warning-color'),
|
||||
Trust.BLIND: ('security-medium-symbolic',
|
||||
_('Blind Trust'),
|
||||
'openpgp-dark-success-color'),
|
||||
Trust.VERIFIED: ('security-high-symbolic',
|
||||
_('Verified'),
|
||||
'success-color')
|
||||
}
|
||||
|
||||
|
||||
class KeyDialog(Gtk.Dialog):
|
||||
def __init__(self, account, jid, transient):
|
||||
flags = Gtk.DialogFlags.DESTROY_WITH_PARENT
|
||||
super().__init__(_('Public Keys for %s') % jid, None, flags)
|
||||
|
||||
self.set_transient_for(transient)
|
||||
self.set_resizable(True)
|
||||
self.set_default_size(500, 300)
|
||||
|
||||
self.get_style_context().add_class('openpgp-key-dialog')
|
||||
|
||||
self.con = app.connections[account]
|
||||
|
||||
self._listbox = Gtk.ListBox()
|
||||
self._listbox.set_selection_mode(Gtk.SelectionMode.NONE)
|
||||
|
||||
self._scrolled = Gtk.ScrolledWindow()
|
||||
self._scrolled.set_policy(Gtk.PolicyType.NEVER,
|
||||
Gtk.PolicyType.AUTOMATIC)
|
||||
self._scrolled.add(self._listbox)
|
||||
|
||||
box = self.get_content_area()
|
||||
box.pack_start(self._scrolled, True, True, 0)
|
||||
|
||||
keys = self.con.get_module('OpenPGP').get_keys(jid, only_trusted=False)
|
||||
for key in keys:
|
||||
log.info('Load: %s', key.fingerprint)
|
||||
self._listbox.add(KeyRow(key))
|
||||
self.show_all()
|
||||
|
||||
|
||||
class KeyRow(Gtk.ListBoxRow):
|
||||
def __init__(self, key):
|
||||
Gtk.ListBoxRow.__init__(self)
|
||||
self.set_activatable(False)
|
||||
|
||||
self._dialog = self.get_toplevel()
|
||||
self.key = key
|
||||
|
||||
box = Gtk.Box()
|
||||
box.set_spacing(12)
|
||||
|
||||
self._trust_button = TrustButton(self)
|
||||
box.add(self._trust_button)
|
||||
|
||||
label_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||
fingerprint = Gtk.Label(
|
||||
label=self._format_fingerprint(key.fingerprint))
|
||||
fingerprint.get_style_context().add_class('openpgp-mono')
|
||||
if not key.active:
|
||||
fingerprint.get_style_context().add_class('openpgp-inactive-color')
|
||||
fingerprint.set_selectable(True)
|
||||
fingerprint.set_halign(Gtk.Align.START)
|
||||
fingerprint.set_valign(Gtk.Align.START)
|
||||
fingerprint.set_hexpand(True)
|
||||
label_box.add(fingerprint)
|
||||
|
||||
date = Gtk.Label(label=self._format_timestamp(key.timestamp))
|
||||
date.set_halign(Gtk.Align.START)
|
||||
date.get_style_context().add_class('openpgp-mono')
|
||||
if not key.active:
|
||||
date.get_style_context().add_class('openpgp-inactive-color')
|
||||
label_box.add(date)
|
||||
|
||||
box.add(label_box)
|
||||
|
||||
self.add(box)
|
||||
self.show_all()
|
||||
|
||||
def delete_fingerprint(self, *args):
|
||||
def _remove():
|
||||
self.get_parent().remove(self)
|
||||
self.key.delete()
|
||||
self.destroy()
|
||||
|
||||
buttons = {
|
||||
Gtk.ResponseType.CANCEL: DialogButton('Cancel'),
|
||||
Gtk.ResponseType.OK: DialogButton('Delete',
|
||||
_remove,
|
||||
ButtonAction.DESTRUCTIVE),
|
||||
}
|
||||
|
||||
NewConfirmationDialog(
|
||||
_('Delete Public Key'),
|
||||
_('This will permanently delete this public key'),
|
||||
buttons,
|
||||
transient_for=self.get_toplevel())
|
||||
|
||||
def set_trust(self, trust):
|
||||
icon_name, tooltip, css_class = TRUST_DATA[trust]
|
||||
image = self._trust_button.get_child()
|
||||
image.set_from_icon_name(icon_name, Gtk.IconSize.MENU)
|
||||
image.get_style_context().add_class(css_class)
|
||||
|
||||
@staticmethod
|
||||
def _format_fingerprint(fingerprint):
|
||||
fplen = len(fingerprint)
|
||||
wordsize = fplen // 8
|
||||
buf = ''
|
||||
for w in range(0, fplen, wordsize):
|
||||
buf += '{0} '.format(fingerprint[w:w + wordsize])
|
||||
return buf.rstrip()
|
||||
|
||||
@staticmethod
|
||||
def _format_timestamp(timestamp):
|
||||
return time.strftime('%Y-%m-%d %H:%M:%S',
|
||||
time.localtime(timestamp))
|
||||
|
||||
|
||||
class TrustButton(Gtk.MenuButton):
|
||||
def __init__(self, row):
|
||||
Gtk.MenuButton.__init__(self)
|
||||
self._row = row
|
||||
self._css_class = ''
|
||||
self.set_popover(TrustPopver(row))
|
||||
self.update()
|
||||
|
||||
def update(self):
|
||||
icon_name, tooltip, css_class = TRUST_DATA[self._row.key.trust]
|
||||
image = self.get_child()
|
||||
image.set_from_icon_name(icon_name, Gtk.IconSize.MENU)
|
||||
# remove old color from icon
|
||||
image.get_style_context().remove_class(self._css_class)
|
||||
|
||||
if not self._row.key.active:
|
||||
css_class = 'openpgp-inactive-color'
|
||||
tooltip = '%s - %s' % (_('Inactive'), tooltip)
|
||||
|
||||
image.get_style_context().add_class(css_class)
|
||||
self._css_class = css_class
|
||||
self.set_tooltip_text(tooltip)
|
||||
|
||||
|
||||
class TrustPopver(Gtk.Popover):
|
||||
def __init__(self, row):
|
||||
Gtk.Popover.__init__(self)
|
||||
self._row = row
|
||||
self._listbox = Gtk.ListBox()
|
||||
self._listbox.set_selection_mode(Gtk.SelectionMode.NONE)
|
||||
if row.key.trust != Trust.VERIFIED:
|
||||
self._listbox.add(VerifiedOption())
|
||||
if row.key.trust != Trust.NOT_TRUSTED:
|
||||
self._listbox.add(NotTrustedOption())
|
||||
self._listbox.add(DeleteOption())
|
||||
self.add(self._listbox)
|
||||
self._listbox.show_all()
|
||||
self._listbox.connect('row-activated', self._activated)
|
||||
self.get_style_context().add_class('openpgp-trust-popover')
|
||||
|
||||
def _activated(self, listbox, row):
|
||||
self.popdown()
|
||||
if row.type_ is None:
|
||||
self._row.delete_fingerprint()
|
||||
else:
|
||||
self._row.key.trust = row.type_
|
||||
self.get_relative_to().update()
|
||||
self.update()
|
||||
|
||||
def update(self):
|
||||
self._listbox.foreach(lambda row: self._listbox.remove(row))
|
||||
if self._row.key.trust != Trust.VERIFIED:
|
||||
self._listbox.add(VerifiedOption())
|
||||
if self._row.key.trust != Trust.NOT_TRUSTED:
|
||||
self._listbox.add(NotTrustedOption())
|
||||
self._listbox.add(DeleteOption())
|
||||
|
||||
|
||||
class MenuOption(Gtk.ListBoxRow):
|
||||
def __init__(self):
|
||||
Gtk.ListBoxRow.__init__(self)
|
||||
box = Gtk.Box()
|
||||
box.set_spacing(6)
|
||||
|
||||
image = Gtk.Image.new_from_icon_name(self.icon,
|
||||
Gtk.IconSize.MENU)
|
||||
label = Gtk.Label(label=self.label)
|
||||
image.get_style_context().add_class(self.color)
|
||||
|
||||
box.add(image)
|
||||
box.add(label)
|
||||
self.add(box)
|
||||
self.show_all()
|
||||
|
||||
|
||||
class VerifiedOption(MenuOption):
|
||||
|
||||
type_ = Trust.VERIFIED
|
||||
icon = 'security-high-symbolic'
|
||||
label = _('Verified')
|
||||
color = 'success-color'
|
||||
|
||||
def __init__(self):
|
||||
MenuOption.__init__(self)
|
||||
|
||||
|
||||
class NotTrustedOption(MenuOption):
|
||||
|
||||
type_ = Trust.NOT_TRUSTED
|
||||
icon = 'dialog-error-symbolic'
|
||||
label = _('Not Trusted')
|
||||
color = 'error-color'
|
||||
|
||||
def __init__(self):
|
||||
MenuOption.__init__(self)
|
||||
|
||||
|
||||
class DeleteOption(MenuOption):
|
||||
|
||||
type_ = None
|
||||
icon = 'user-trash-symbolic'
|
||||
label = _('Delete')
|
||||
color = ''
|
||||
|
||||
def __init__(self):
|
||||
MenuOption.__init__(self)
|
||||
17
openpgp/gtk/style.css
Normal file
17
openpgp/gtk/style.css
Normal file
@@ -0,0 +1,17 @@
|
||||
.openpgp-dark-success-color { color: darker(@success_color); }
|
||||
.openpgp-inactive-color { color: @unfocused_borders; }
|
||||
|
||||
.openpgp-mono { font-size: 12px; font-family: monospace; }
|
||||
|
||||
.openpgp-key-dialog > box { margin: 12px; }
|
||||
|
||||
.openpgp-key-dialog scrolledwindow row {
|
||||
border-bottom: 1px solid;
|
||||
border-color: @unfocused_borders;
|
||||
padding: 10px 20px 10px 10px;
|
||||
}
|
||||
.openpgp-key-dialog scrolledwindow row:last-child { border-bottom: 0px}
|
||||
|
||||
.openpgp-key-dialog scrolledwindow { border: 1px solid; border-color:@unfocused_borders; }
|
||||
|
||||
.openpgp-trust-popover row { padding: 10px 15px 10px 10px; }
|
||||
253
openpgp/gtk/wizard.py
Normal file
253
openpgp/gtk/wizard.py
Normal file
@@ -0,0 +1,253 @@
|
||||
# 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 threading
|
||||
from enum import IntEnum
|
||||
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import GLib
|
||||
|
||||
from gajim.common import app
|
||||
|
||||
log = logging.getLogger('gajim.plugin_system.openpgp.wizard')
|
||||
|
||||
|
||||
class Page(IntEnum):
|
||||
WELCOME = 0
|
||||
NEWKEY = 1
|
||||
SUCCESS = 2
|
||||
ERROR = 3
|
||||
|
||||
|
||||
class KeyWizard(Gtk.Assistant):
|
||||
def __init__(self, plugin, account, chat_control):
|
||||
Gtk.Assistant.__init__(self)
|
||||
|
||||
self._con = app.connections[account]
|
||||
self._plugin = plugin
|
||||
self._account = account
|
||||
self._data_form_widget = None
|
||||
self._is_form = None
|
||||
self._chat_control = chat_control
|
||||
|
||||
self.set_application(app.app)
|
||||
self.set_transient_for(chat_control.parent_win.window)
|
||||
self.set_resizable(True)
|
||||
self.set_position(Gtk.WindowPosition.CENTER)
|
||||
|
||||
self.set_default_size(600, 400)
|
||||
self.get_style_context().add_class('dialog-margin')
|
||||
|
||||
self._add_page(WelcomePage())
|
||||
# self._add_page(BackupKeyPage())
|
||||
self._add_page(NewKeyPage(self, self._con))
|
||||
# self._add_page(SaveBackupCodePage())
|
||||
self._add_page(SuccessfulPage())
|
||||
self._add_page(ErrorPage())
|
||||
|
||||
self.connect('prepare', self._on_page_change)
|
||||
self.connect('cancel', self._on_cancel)
|
||||
self.connect('close', self._on_cancel)
|
||||
|
||||
self._remove_sidebar()
|
||||
self.show_all()
|
||||
|
||||
def _add_page(self, page):
|
||||
self.append_page(page)
|
||||
self.set_page_type(page, page.type_)
|
||||
self.set_page_title(page, page.title)
|
||||
self.set_page_complete(page, page.complete)
|
||||
|
||||
def _remove_sidebar(self):
|
||||
main_box = self.get_children()[0]
|
||||
sidebar = main_box.get_children()[0]
|
||||
main_box.remove(sidebar)
|
||||
|
||||
def _activate_encryption(self):
|
||||
win = self._chat_control.parent_win.window
|
||||
action = win.lookup_action(
|
||||
'set-encryption-%s' % self._chat_control.control_id)
|
||||
action.activate(GLib.Variant("s", self._plugin.encryption_name))
|
||||
|
||||
def _on_page_change(self, assistant, page):
|
||||
if self.get_current_page() == Page.NEWKEY:
|
||||
if self._con.get_module('OpenPGP').secret_key_available:
|
||||
self.set_current_page(Page.SUCCESS)
|
||||
else:
|
||||
page.generate()
|
||||
elif self.get_current_page() == Page.SUCCESS:
|
||||
self._activate_encryption()
|
||||
|
||||
def _on_error(self, error_text):
|
||||
log.info('Show Error page')
|
||||
page = self.get_nth_page(Page.ERROR)
|
||||
page.set_text(error_text)
|
||||
self.set_current_page(Page.ERROR)
|
||||
|
||||
def _on_cancel(self, widget):
|
||||
self.destroy()
|
||||
|
||||
|
||||
class WelcomePage(Gtk.Box):
|
||||
|
||||
type_ = Gtk.AssistantPageType.INTRO
|
||||
title = _('Welcome')
|
||||
complete = True
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(orientation=Gtk.Orientation.VERTICAL)
|
||||
self.set_spacing(18)
|
||||
title_label = Gtk.Label(label=_('Setup OpenPGP'))
|
||||
text_label = Gtk.Label(
|
||||
label=_('Gajim will now try to setup OpenPGP for you'))
|
||||
self.add(title_label)
|
||||
self.add(text_label)
|
||||
|
||||
|
||||
class RequestPage(Gtk.Box):
|
||||
|
||||
type_ = Gtk.AssistantPageType.INTRO
|
||||
title = _('Request OpenPGP Key')
|
||||
complete = False
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(orientation=Gtk.Orientation.VERTICAL)
|
||||
self.set_spacing(18)
|
||||
spinner = Gtk.Spinner()
|
||||
self.pack_start(spinner, True, True, 0)
|
||||
spinner.start()
|
||||
|
||||
|
||||
# class BackupKeyPage(Gtk.Box):
|
||||
|
||||
# type_ = Gtk.AssistantPageType.INTRO
|
||||
# title = _('Supply Backup Code')
|
||||
# complete = True
|
||||
|
||||
# def __init__(self):
|
||||
# super().__init__(orientation=Gtk.Orientation.VERTICAL)
|
||||
# self.set_spacing(18)
|
||||
# title_label = Gtk.Label(label=_('Backup Code'))
|
||||
# text_label = Gtk.Label(
|
||||
# label=_('We found a backup Code, please supply your password'))
|
||||
# self.add(title_label)
|
||||
# self.add(text_label)
|
||||
# entry = Gtk.Entry()
|
||||
# self.add(entry)
|
||||
|
||||
|
||||
class NewKeyPage(RequestPage):
|
||||
|
||||
type_ = Gtk.AssistantPageType.PROGRESS
|
||||
title = _('Generating new Key')
|
||||
complete = False
|
||||
|
||||
def __init__(self, assistant, con):
|
||||
super().__init__()
|
||||
self._assistant = assistant
|
||||
self._con = con
|
||||
|
||||
def generate(self):
|
||||
log.info('Creating Key')
|
||||
thread = threading.Thread(target=self.worker)
|
||||
thread.start()
|
||||
|
||||
def worker(self):
|
||||
error = None
|
||||
try:
|
||||
self._con.get_module('OpenPGP').generate_key()
|
||||
except Exception as e:
|
||||
error = e
|
||||
else:
|
||||
self._con.get_module('OpenPGP').get_own_key_details()
|
||||
self._con.get_module('OpenPGP').publish_key()
|
||||
self._con.get_module('OpenPGP').query_key_list()
|
||||
GLib.idle_add(self.finished, error)
|
||||
|
||||
def finished(self, error):
|
||||
if error is None:
|
||||
self._assistant.set_current_page(Page.SUCCESS)
|
||||
else:
|
||||
log.error(error)
|
||||
self._assistant.set_current_page(Page.ERROR)
|
||||
|
||||
|
||||
# class SaveBackupCodePage(RequestPage):
|
||||
|
||||
# type_ = Gtk.AssistantPageType.PROGRESS
|
||||
# title = _('Save this code')
|
||||
# complete = False
|
||||
|
||||
# def __init__(self):
|
||||
# super().__init__(orientation=Gtk.Orientation.VERTICAL)
|
||||
# self.set_spacing(18)
|
||||
# title_label = Gtk.Label(label=_('Backup Code'))
|
||||
# text_label = Gtk.Label(
|
||||
# label=_('This is your backup code, you need it if you reinstall Gajim'))
|
||||
# self.add(title_label)
|
||||
# self.add(text_label)
|
||||
|
||||
|
||||
class SuccessfulPage(Gtk.Box):
|
||||
|
||||
type_ = Gtk.AssistantPageType.SUMMARY
|
||||
title = _('Setup successful')
|
||||
complete = True
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(orientation=Gtk.Orientation.VERTICAL)
|
||||
self.set_spacing(12)
|
||||
self.set_homogeneous(True)
|
||||
|
||||
icon = Gtk.Image.new_from_icon_name('object-select-symbolic',
|
||||
Gtk.IconSize.DIALOG)
|
||||
icon.get_style_context().add_class('success-color')
|
||||
icon.set_valign(Gtk.Align.END)
|
||||
label = Gtk.Label(label=_('Setup successful'))
|
||||
label.get_style_context().add_class('bold16')
|
||||
label.set_valign(Gtk.Align.START)
|
||||
|
||||
self.add(icon)
|
||||
self.add(label)
|
||||
|
||||
|
||||
class ErrorPage(Gtk.Box):
|
||||
|
||||
type_ = Gtk.AssistantPageType.SUMMARY
|
||||
title = _('Registration failed')
|
||||
complete = True
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(orientation=Gtk.Orientation.VERTICAL)
|
||||
self.set_spacing(12)
|
||||
self.set_homogeneous(True)
|
||||
|
||||
icon = Gtk.Image.new_from_icon_name('dialog-error-symbolic',
|
||||
Gtk.IconSize.DIALOG)
|
||||
icon.get_style_context().add_class('error-color')
|
||||
icon.set_valign(Gtk.Align.END)
|
||||
self._label = Gtk.Label()
|
||||
self._label.get_style_context().add_class('bold16')
|
||||
self._label.set_valign(Gtk.Align.START)
|
||||
|
||||
self.add(icon)
|
||||
self.add(self._label)
|
||||
|
||||
def set_text(self, text):
|
||||
self._label.set_text(text)
|
||||
8
openpgp/manifest.ini
Normal file
8
openpgp/manifest.ini
Normal file
@@ -0,0 +1,8 @@
|
||||
[info]
|
||||
name: OpenPGP
|
||||
short_name: openpgp
|
||||
version: 0.90.0
|
||||
description: Experimental OpenPGP XEP-0373 Implementation
|
||||
authors: Philipp Hörist <philipp@hoerist.com>
|
||||
homepage: https://dev.gajim.org/gajim/gajim-plugins/wikis/pgp
|
||||
min_gajim_version: 1.0.99
|
||||
0
openpgp/modules/__init__.py
Normal file
0
openpgp/modules/__init__.py
Normal file
538
openpgp/modules/openpgp.py
Normal file
538
openpgp/modules/openpgp.py
Normal file
@@ -0,0 +1,538 @@
|
||||
# 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 time
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from base64 import b64decode, b64encode
|
||||
|
||||
from nbxmpp import Node, isResultNode
|
||||
|
||||
from gajim.common import app
|
||||
from gajim.common import configpaths
|
||||
from gajim.common.connection_handlers_events import MessageNotSentEvent
|
||||
|
||||
from openpgp.modules import util
|
||||
from openpgp.modules.util import NS_OPENPGP_PUBLIC_KEYS
|
||||
from openpgp.modules.util import NS_OPENPGP
|
||||
from openpgp.modules.util import Key
|
||||
from openpgp.modules.util import Trust
|
||||
from openpgp.modules.util import VerifyFailed
|
||||
from openpgp.modules.util import DecryptionFailed
|
||||
from openpgp.backend.sql import Storage
|
||||
from openpgp.backend.pygpg import PGPContext
|
||||
|
||||
|
||||
log = logging.getLogger('gajim.plugin_system.openpgp')
|
||||
|
||||
|
||||
ENCRYPTION_NAME = 'OpenPGP'
|
||||
|
||||
# Module name
|
||||
name = 'OpenPGP'
|
||||
zeroconf = False
|
||||
|
||||
|
||||
class KeyData:
|
||||
'''
|
||||
Holds all data related to a certain key
|
||||
'''
|
||||
def __init__(self, contact_data):
|
||||
self._contact_data = contact_data
|
||||
self.fingerprint = None
|
||||
self.active = False
|
||||
self._trust = Trust.UNKNOWN
|
||||
self.timestamp = None
|
||||
self.comment = None
|
||||
self.has_pubkey = False
|
||||
|
||||
@property
|
||||
def trust(self):
|
||||
return self._trust
|
||||
|
||||
@trust.setter
|
||||
def trust(self, value):
|
||||
if value not in (Trust.NOT_TRUSTED,
|
||||
Trust.UNKNOWN,
|
||||
Trust.BLIND,
|
||||
Trust.VERIFIED):
|
||||
raise ValueError('Trust value not allowed: %s' % value)
|
||||
self._trust = value
|
||||
self._contact_data.set_trust(self.fingerprint, self._trust)
|
||||
|
||||
@classmethod
|
||||
def from_key(cls, contact_data, key, trust):
|
||||
keydata = cls(contact_data)
|
||||
keydata.fingerprint = key.fingerprint
|
||||
keydata.timestamp = key.date
|
||||
keydata.active = True
|
||||
keydata._trust = trust
|
||||
return keydata
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, contact_data, row):
|
||||
keydata = cls(contact_data)
|
||||
keydata.fingerprint = row.fingerprint
|
||||
keydata.timestamp = row.timestamp
|
||||
keydata.comment = row.comment
|
||||
keydata._trust = row.trust
|
||||
keydata.active = row.active
|
||||
return keydata
|
||||
|
||||
def delete(self):
|
||||
self._contact_data.delete_key(self.fingerprint)
|
||||
|
||||
|
||||
class ContactData:
|
||||
'''
|
||||
Holds all data related to a contact
|
||||
'''
|
||||
def __init__(self, jid, storage, pgp):
|
||||
self.jid = jid
|
||||
self._key_store = {}
|
||||
self._storage = storage
|
||||
self._pgp = pgp
|
||||
|
||||
@property
|
||||
def userid(self):
|
||||
if self._jid is None:
|
||||
raise ValueError('JID not set')
|
||||
return 'xmpp:%s' % self._jid
|
||||
|
||||
@property
|
||||
def default_trust(self):
|
||||
for key in self._key_store:
|
||||
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',
|
||||
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 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 []
|
||||
|
||||
|
||||
class OpenPGP:
|
||||
def __init__(self, con):
|
||||
self._con = con
|
||||
self._account = con.name
|
||||
|
||||
self.handlers = []
|
||||
|
||||
self.own_jid = self.get_own_jid(stripped=True)
|
||||
|
||||
path = Path(configpaths.get('MY_DATA')) / 'openpgp' / self.own_jid
|
||||
if not path.exists():
|
||||
path.mkdir(parents=True)
|
||||
|
||||
self._pgp = PGPContext(self.own_jid, path)
|
||||
self._storage = Storage(path)
|
||||
self._contacts = PGPContacts(self._pgp, self._storage)
|
||||
self._fingerprint, self._date = self.get_own_key_details()
|
||||
log.info('Own Fingerprint at start: %s', self._fingerprint)
|
||||
|
||||
@property
|
||||
def secret_key_available(self):
|
||||
return self._fingerprint is not None
|
||||
|
||||
def get_own_jid(self, stripped=False):
|
||||
if stripped:
|
||||
return self._con.get_own_jid().getStripped()
|
||||
return self._con.get_own_jid()
|
||||
|
||||
def get_own_key_details(self):
|
||||
self._fingerprint, self._date = self._pgp.get_own_key_details()
|
||||
return self._fingerprint, self._date
|
||||
|
||||
def generate_key(self):
|
||||
self._pgp.generate_key()
|
||||
|
||||
def publish_key(self):
|
||||
log.info('%s => Publish key', self._account)
|
||||
key = self._pgp.export_key(self._fingerprint)
|
||||
|
||||
date = time.strftime(
|
||||
'%Y-%m-%dT%H:%M:%SZ', time.gmtime(self._date))
|
||||
pubkey_node = Node('pubkey', attrs={'xmlns': NS_OPENPGP,
|
||||
'date': date})
|
||||
data = pubkey_node.addChild('data')
|
||||
data.addData(b64encode(key).decode('utf8'))
|
||||
node = '%s:%s' % (NS_OPENPGP_PUBLIC_KEYS, self._fingerprint)
|
||||
|
||||
self._con.get_module('PubSub').send_pb_publish(
|
||||
self.own_jid, node, pubkey_node,
|
||||
id_='current', cb=self._public_result)
|
||||
|
||||
def _publish_key_list(self, keylist=None):
|
||||
if keylist is None:
|
||||
keylist = [Key(self._fingerprint, self._date)]
|
||||
log.info('%s => Publish keys list', self._account)
|
||||
self._con.get_module('PGPKeylist').send(keylist)
|
||||
|
||||
def _public_result(self, con, stanza):
|
||||
if not isResultNode(stanza):
|
||||
log.error('%s => Publishing failed: %s',
|
||||
self._account, stanza.getError())
|
||||
|
||||
def _query_public_key(self, jid, fingerprint):
|
||||
log.info('%s => Fetch public key %s - %s',
|
||||
self._account, fingerprint, jid)
|
||||
node = '%s:%s' % (NS_OPENPGP_PUBLIC_KEYS, fingerprint)
|
||||
self._con.get_module('PubSub').send_pb_retrieve(
|
||||
jid, node, cb=self._public_key_received, fingerprint=fingerprint)
|
||||
|
||||
def _public_key_received(self, con, stanza, fingerprint):
|
||||
if not isResultNode(stanza):
|
||||
log.error('%s => Public Key not found: %s',
|
||||
self._account, stanza.getError())
|
||||
return
|
||||
pubkey = util.unpack_public_key(stanza, fingerprint)
|
||||
if pubkey is None:
|
||||
log.warning('Invalid public key received:\n%s', stanza)
|
||||
return
|
||||
|
||||
jid = stanza.getFrom().getStripped()
|
||||
result = self._pgp.import_key(pubkey, jid)
|
||||
if result is not None:
|
||||
self._contacts.set_public_key(jid, fingerprint)
|
||||
|
||||
def query_key_list(self, jid=None):
|
||||
if jid is None:
|
||||
jid = self.own_jid
|
||||
log.info('%s => Fetch keys list %s', self._account, jid)
|
||||
self._con.get_module('PubSub').send_pb_retrieve(
|
||||
jid, NS_OPENPGP_PUBLIC_KEYS,
|
||||
cb=self._query_key_list_result)
|
||||
|
||||
def _query_key_list_result(self, con, stanza):
|
||||
from_jid = stanza.getFrom()
|
||||
if from_jid is None:
|
||||
from_jid = self.own_jid
|
||||
else:
|
||||
from_jid = from_jid.getStripped()
|
||||
|
||||
if not isResultNode(stanza):
|
||||
log.error('%s => Keys list query failed: %s',
|
||||
self._account, stanza.getError())
|
||||
if from_jid == self.own_jid and self._fingerprint is not None:
|
||||
self._publish_key_list()
|
||||
return
|
||||
|
||||
from_jid = stanza.getFrom()
|
||||
if from_jid is None:
|
||||
from_jid = self.own_jid
|
||||
else:
|
||||
from_jid = from_jid.getStripped()
|
||||
|
||||
keylist = util.unpack_public_key_list(stanza, from_jid)
|
||||
self.key_list_received(keylist, from_jid)
|
||||
|
||||
def key_list_received(self, keylist, from_jid):
|
||||
if keylist is None:
|
||||
log.warning('Invalid keys list received')
|
||||
if from_jid == self.own_jid and self._fingerprint is not None:
|
||||
self._publish_key_list()
|
||||
return
|
||||
|
||||
if not keylist:
|
||||
log.warning('%s => Empty keys list received from %s',
|
||||
self._account, from_jid)
|
||||
self._contacts.process_keylist(self.own_jid, keylist)
|
||||
if from_jid == self.own_jid and self._fingerprint is not None:
|
||||
self._publish_key_list()
|
||||
return
|
||||
|
||||
if from_jid == self.own_jid:
|
||||
log.info('Received own keys list')
|
||||
for key in keylist:
|
||||
log.info(key.fingerprint)
|
||||
for key in keylist:
|
||||
# Check if own fingerprint is published
|
||||
if key.fingerprint == self._fingerprint:
|
||||
log.info('Own key found in keys list')
|
||||
return
|
||||
log.info('Own key not published')
|
||||
if self._fingerprint is not None:
|
||||
keylist.append(Key(self._fingerprint, None))
|
||||
self._publish_key_list(keylist)
|
||||
return
|
||||
|
||||
missing_pub_keys = self._contacts.process_keylist(from_jid, keylist)
|
||||
|
||||
for key in keylist:
|
||||
log.info(key.fingerprint)
|
||||
|
||||
for fingerprint in missing_pub_keys:
|
||||
self._query_public_key(from_jid, fingerprint)
|
||||
|
||||
def decrypt_message(self, obj, callback):
|
||||
if obj.encrypted:
|
||||
# Another Plugin already decrypted the message
|
||||
return
|
||||
|
||||
if obj.name == 'message-received':
|
||||
enc_tag = obj.stanza.getTag('openpgp', namespace=NS_OPENPGP)
|
||||
jid = obj.jid
|
||||
else:
|
||||
enc_tag = obj.message.getTag('openpgp', namespace=NS_OPENPGP)
|
||||
jid = obj.with_
|
||||
|
||||
if enc_tag is None:
|
||||
return
|
||||
|
||||
log.info('Received OpenPGP message from: %s', jid)
|
||||
b64encode_payload = enc_tag.getData()
|
||||
encrypted_payload = b64decode(b64encode_payload)
|
||||
|
||||
try:
|
||||
decrypted_payload = self._pgp.decrypt(encrypted_payload)
|
||||
except DecryptionFailed as error:
|
||||
log.warning(error)
|
||||
return
|
||||
|
||||
signcrypt = Node(node=decrypted_payload)
|
||||
|
||||
signcrypt_jid = signcrypt.getTagAttr('to', 'jid')
|
||||
if self.own_jid != signcrypt_jid:
|
||||
log.warning('signcrypt "to" attr %s != %s',
|
||||
self.own_jid, signcrypt_jid)
|
||||
log.debug(signcrypt)
|
||||
return
|
||||
|
||||
payload = signcrypt.getTag('payload')
|
||||
|
||||
body = None
|
||||
if obj.name == 'message-received':
|
||||
obj.stanza.delChild(enc_tag)
|
||||
for node in payload.getChildren():
|
||||
if node.name == 'body':
|
||||
body = node.getData()
|
||||
obj.stanza.setTagData('body', body)
|
||||
else:
|
||||
obj.stanza.addChild(node=node)
|
||||
else:
|
||||
obj.msg_.delChild(enc_tag)
|
||||
for node in payload.getChildren():
|
||||
if node.name == 'body':
|
||||
body = node.getData()
|
||||
obj.msg_.setTagData('body', node.getData())
|
||||
else:
|
||||
obj.msg_.addChild(node=node)
|
||||
|
||||
if body:
|
||||
obj.msgtxt = body
|
||||
|
||||
obj.encrypted = ENCRYPTION_NAME
|
||||
callback(obj)
|
||||
|
||||
def encrypt_message(self, obj, callback):
|
||||
keys = self._contacts.get_keys(obj.jid)
|
||||
if not keys:
|
||||
# TODO: this should never happen in theory
|
||||
log.error('Droping stanza to %s, because we have no key', obj.jid)
|
||||
return
|
||||
|
||||
keys += self._contacts.get_keys(self.own_jid)
|
||||
keys += [Key(self._fingerprint, None)]
|
||||
|
||||
payload = util.create_signcrypt_node(obj)
|
||||
|
||||
encrypted_payload, error = self._pgp.encrypt(payload, keys)
|
||||
if error:
|
||||
log.error('Error: %s', error)
|
||||
app.nec.push_incoming_event(
|
||||
MessageNotSentEvent(
|
||||
None, conn=self._con, jid=obj.jid, message=obj.message,
|
||||
error=error, time_=time.time()))
|
||||
return
|
||||
|
||||
util.create_openpgp_message(obj, encrypted_payload)
|
||||
|
||||
obj.encrypted = ENCRYPTION_NAME
|
||||
self.print_msg_to_log(obj.msg_iq)
|
||||
callback(obj)
|
||||
|
||||
@staticmethod
|
||||
def print_msg_to_log(stanza):
|
||||
""" Prints a stanza in a fancy way to the log """
|
||||
log.debug('-'*15)
|
||||
stanzastr = '\n' + stanza.__str__(fancy=True)
|
||||
stanzastr = stanzastr[0:-1]
|
||||
log.debug(stanzastr)
|
||||
log.debug('-'*15)
|
||||
|
||||
def get_keys(self, jid=None, only_trusted=True):
|
||||
if jid is None:
|
||||
jid = self.own_jid
|
||||
return self._contacts.get_keys(jid, only_trusted=only_trusted)
|
||||
|
||||
def clear_fingerprints(self):
|
||||
self._publish_key_list()
|
||||
|
||||
def cleanup(self):
|
||||
self._storage.cleanup()
|
||||
self._pgp = None
|
||||
self._contacts = None
|
||||
|
||||
|
||||
def get_instance(*args, **kwargs):
|
||||
return OpenPGP(*args, **kwargs), 'OpenPGP'
|
||||
124
openpgp/modules/pgp_keylist.py
Normal file
124
openpgp/modules/pgp_keylist.py
Normal file
@@ -0,0 +1,124 @@
|
||||
# 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'
|
||||
|
||||
def __init__(self, keylist):
|
||||
self._pep_specific_data = keylist
|
||||
self.data = 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 __init__(self, con):
|
||||
AbstractPEPModule.__init__(self, con, con.name)
|
||||
|
||||
self.handlers = []
|
||||
|
||||
def _extract_info(self, item):
|
||||
keylist_tag = item.getTag('public-keys-list',
|
||||
namespace=util.NS_OPENPGP)
|
||||
if keylist_tag is None:
|
||||
raise StanzaMalformed('No public-keys-list node')
|
||||
|
||||
metadata = keylist_tag.getTags('pubkey-metadata')
|
||||
if not metadata:
|
||||
raise StanzaMalformed('No metadata found')
|
||||
|
||||
keylist = []
|
||||
for data in metadata:
|
||||
attrs = data.getAttrs()
|
||||
|
||||
if not attrs or 'v4-fingerprint' not in attrs:
|
||||
raise StanzaMalformed('No fingerprint in metadata')
|
||||
|
||||
date = attrs.get('date', None)
|
||||
if date is None:
|
||||
raise StanzaMalformed('No date in metadata')
|
||||
else:
|
||||
timestamp = parse_datetime(date, epoch=True)
|
||||
if timestamp is None:
|
||||
raise StanzaMalformed('Invalid date timestamp: %s', date)
|
||||
|
||||
keylist.append(Key(attrs['v4-fingerprint'], 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'
|
||||
205
openpgp/modules/util.py
Normal file
205
openpgp/modules/util.py
Normal file
@@ -0,0 +1,205 @@
|
||||
# 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 random
|
||||
import time
|
||||
import string
|
||||
from enum import IntEnum
|
||||
from collections import namedtuple
|
||||
from base64 import b64decode, b64encode
|
||||
|
||||
import nbxmpp
|
||||
from nbxmpp import Node
|
||||
|
||||
NS_OPENPGP = 'urn:xmpp:openpgp:0'
|
||||
NS_OPENPGP_PUBLIC_KEYS = 'urn:xmpp:openpgp:0:public-keys'
|
||||
NS_NOTIFY = NS_OPENPGP_PUBLIC_KEYS + '+notify'
|
||||
|
||||
NOT_ENCRYPTED_TAGS = [('no-store', nbxmpp.NS_MSG_HINTS),
|
||||
('store', nbxmpp.NS_MSG_HINTS),
|
||||
('no-copy', nbxmpp.NS_MSG_HINTS),
|
||||
('no-permanent-store', nbxmpp.NS_MSG_HINTS),
|
||||
('thread', None)]
|
||||
|
||||
Key = namedtuple('Key', 'fingerprint date')
|
||||
|
||||
log = logging.getLogger('gajim.plugin_system.openpgp.util')
|
||||
|
||||
|
||||
class Trust(IntEnum):
|
||||
NOT_TRUSTED = 0
|
||||
UNKNOWN = 1
|
||||
BLIND = 2
|
||||
VERIFIED = 3
|
||||
|
||||
|
||||
def unpack_public_key_list(stanza, from_jid):
|
||||
fingerprints = []
|
||||
|
||||
parent = stanza.getTag('pubsub', namespace=nbxmpp.NS_PUBSUB)
|
||||
if parent is None:
|
||||
parent = stanza.getTag('event', namespace=nbxmpp.NS_PUBSUB_EVENT)
|
||||
if parent is None:
|
||||
log.warning('PGP keys list has no pubsub/event node')
|
||||
return
|
||||
|
||||
items = parent.getTag('items', attrs={'node': NS_OPENPGP_PUBLIC_KEYS})
|
||||
if items is None:
|
||||
log.warning('PGP keys list has no items node')
|
||||
return
|
||||
|
||||
item = items.getTags('item')
|
||||
if not item:
|
||||
log.warning('PGP keys list has no item node')
|
||||
return
|
||||
|
||||
if len(item) > 1:
|
||||
log.warning('PGP keys list has more than one item')
|
||||
return
|
||||
|
||||
key_list = item[0].getTag('public-keys-list', namespace=NS_OPENPGP)
|
||||
if key_list is None:
|
||||
log.warning('PGP keys list has no public-keys-list node')
|
||||
return
|
||||
|
||||
metadata = key_list.getTags('pubkey-metadata')
|
||||
if not metadata:
|
||||
return []
|
||||
|
||||
for node in metadata:
|
||||
attrs = node.getAttrs()
|
||||
if 'v4-fingerprint' not in attrs:
|
||||
log.warning('No fingerprint in metadata node')
|
||||
return
|
||||
|
||||
date = attrs.get('date', None)
|
||||
|
||||
fingerprints.append(
|
||||
Key(attrs['v4-fingerprint'], date))
|
||||
|
||||
return fingerprints
|
||||
|
||||
|
||||
def unpack_public_key(stanza, fingerprint):
|
||||
pubsub = stanza.getTag('pubsub', namespace=nbxmpp.NS_PUBSUB)
|
||||
if pubsub is None:
|
||||
log.warning('PGP public key has no pubsub node')
|
||||
return
|
||||
node = '%s:%s' % (NS_OPENPGP_PUBLIC_KEYS, fingerprint)
|
||||
items = pubsub.getTag('items', attrs={'node': node})
|
||||
if items is None:
|
||||
log.warning('PGP public key has no items node')
|
||||
return
|
||||
|
||||
item = items.getTags('item')
|
||||
if not item:
|
||||
log.warning('PGP public key has no item node')
|
||||
return
|
||||
|
||||
if len(item) > 1:
|
||||
log.warning('PGP public key has more than one item')
|
||||
return
|
||||
|
||||
pub_key = item[0].getTag('pubkey', namespace=NS_OPENPGP)
|
||||
if pub_key is None:
|
||||
log.warning('PGP public key has no pubkey node')
|
||||
return
|
||||
|
||||
data = pub_key.getTag('data')
|
||||
if data is None:
|
||||
log.warning('PGP public key has no data node')
|
||||
return
|
||||
|
||||
return b64decode(data.getData().encode('utf8'))
|
||||
|
||||
|
||||
def create_signcrypt_node(obj):
|
||||
'''
|
||||
<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.encode('utf-8')).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`]'
|
||||
|
||||
|
||||
class VerifyFailed(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class DecryptionFailed(Exception):
|
||||
pass
|
||||
194
openpgp/pgpplugin.py
Normal file
194
openpgp/pgpplugin.py
Normal file
@@ -0,0 +1,194 @@
|
||||
# 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 os
|
||||
from pathlib import Path
|
||||
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import Gdk
|
||||
|
||||
from gajim.plugins import GajimPlugin
|
||||
from gajim.common import app
|
||||
from gajim.common import ged
|
||||
from gajim.common import configpaths
|
||||
from gajim.common import helpers
|
||||
from gajim.common.const import CSSPriority
|
||||
from gajim.gtk.dialogs import ErrorDialog
|
||||
|
||||
from openpgp.modules.util import NS_NOTIFY
|
||||
from openpgp.modules import pgp_keylist
|
||||
try:
|
||||
from openpgp.modules import openpgp
|
||||
except ImportError as e:
|
||||
ERROR_MSG = str(e)
|
||||
else:
|
||||
ERROR_MSG = None
|
||||
|
||||
log = logging.getLogger('gajim.plugin_system.openpgp')
|
||||
|
||||
|
||||
#TODO: we cant encrypt "thread" right now, because its needed for Gajim to find ChatControls.
|
||||
|
||||
class OpenPGPPlugin(GajimPlugin):
|
||||
def init(self):
|
||||
if ERROR_MSG:
|
||||
self.activatable = False
|
||||
self.available_text = ERROR_MSG
|
||||
self.config_dialog = None
|
||||
return
|
||||
|
||||
self.events_handlers = {
|
||||
'signed-in': (ged.PRECORE, self.signed_in),
|
||||
}
|
||||
|
||||
self.modules = [pgp_keylist,
|
||||
openpgp]
|
||||
|
||||
self.encryption_name = 'OpenPGP'
|
||||
self.config_dialog = None
|
||||
self.gui_extension_points = {
|
||||
'encrypt' + self.encryption_name: (self._encrypt_message, None),
|
||||
'decrypt': (self._decrypt_message, None),
|
||||
'send_message' + self.encryption_name: (
|
||||
self._before_sendmessage, None),
|
||||
'encryption_dialog' + self.encryption_name: (
|
||||
self.on_encryption_button_clicked, None),
|
||||
'encryption_state' + self.encryption_name: (
|
||||
self.encryption_state, None),
|
||||
'update_caps': (self._update_caps, None),
|
||||
}
|
||||
|
||||
self.connections = {}
|
||||
|
||||
self.plugin = self
|
||||
self.announced = []
|
||||
self.own_key = None
|
||||
self.pgp_instances = {}
|
||||
self._create_paths()
|
||||
self._load_css()
|
||||
|
||||
def _load_css(self):
|
||||
path = Path(__file__).parent / 'gtk' / 'style.css'
|
||||
try:
|
||||
with open(path, "r") as f:
|
||||
css = f.read()
|
||||
except Exception as exc:
|
||||
log.error('Error loading css: %s', exc)
|
||||
return
|
||||
|
||||
try:
|
||||
provider = Gtk.CssProvider()
|
||||
provider.load_from_data(bytes(css.encode('utf-8')))
|
||||
Gtk.StyleContext.add_provider_for_screen(Gdk.Screen.get_default(),
|
||||
provider,
|
||||
CSSPriority.DEFAULT_THEME)
|
||||
except Exception:
|
||||
log.exception('Error loading application css')
|
||||
|
||||
def _create_paths(self):
|
||||
keyring_path = os.path.join(configpaths.get('MY_DATA'), 'openpgp')
|
||||
if not os.path.exists(keyring_path):
|
||||
os.makedirs(keyring_path)
|
||||
|
||||
def signed_in(self, event):
|
||||
account = event.conn.name
|
||||
con = app.connections[account]
|
||||
if con.get_module('OpenPGP').secret_key_available:
|
||||
log.info('%s => Publish keylist and public key after sign in',
|
||||
account)
|
||||
con.get_module('OpenPGP').query_key_list()
|
||||
con.get_module('OpenPGP').publish_key()
|
||||
|
||||
def activate(self):
|
||||
for account in app.connections:
|
||||
if app.caps_hash[account] != '':
|
||||
# Gajim has already a caps hash calculated, update it
|
||||
helpers.update_optional_features(account)
|
||||
|
||||
con = app.connections[account]
|
||||
if app.account_is_connected(account):
|
||||
if con.get_module('OpenPGP').secret_key_available:
|
||||
log.info('%s => Publish keylist and public key '
|
||||
'after plugin activation', account)
|
||||
con.get_module('OpenPGP').query_key_list()
|
||||
con.get_module('OpenPGP').publish_key()
|
||||
|
||||
def deactivate(self):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def _update_caps(account):
|
||||
if NS_NOTIFY not in app.gajim_optional_features[account]:
|
||||
app.gajim_optional_features[account].append(NS_NOTIFY)
|
||||
|
||||
def activate_encryption(self, chat_control):
|
||||
account = chat_control.account
|
||||
jid = chat_control.contact.jid
|
||||
con = app.connections[account]
|
||||
if con.get_module('OpenPGP').secret_key_available:
|
||||
keys = app.connections[account].get_module('OpenPGP').get_keys(
|
||||
jid, only_trusted=False)
|
||||
if not keys:
|
||||
ErrorDialog(
|
||||
_('No OpenPGP key'),
|
||||
_('We didnt receive a OpenPGP key from this contact.'))
|
||||
return
|
||||
return True
|
||||
else:
|
||||
from openpgp.gtk.wizard import KeyWizard
|
||||
KeyWizard(self, account, chat_control)
|
||||
|
||||
def encryption_state(self, chat_control, state):
|
||||
state['authenticated'] = True
|
||||
state['visible'] = True
|
||||
|
||||
def on_encryption_button_clicked(self, chat_control):
|
||||
account = chat_control.account
|
||||
jid = chat_control.contact.jid
|
||||
transient = chat_control.parent_win.window
|
||||
|
||||
from openpgp.gtk.key import KeyDialog
|
||||
KeyDialog(account, jid, transient)
|
||||
|
||||
def _before_sendmessage(self, chat_control):
|
||||
account = chat_control.account
|
||||
jid = chat_control.contact.jid
|
||||
con = app.connections[account]
|
||||
|
||||
if not con.get_module('OpenPGP').secret_key_available:
|
||||
from openpgp.gtk.wizard import KeyWizard
|
||||
KeyWizard(self, account, chat_control)
|
||||
return
|
||||
|
||||
keys = con.get_module('OpenPGP').get_keys(jid)
|
||||
if not keys:
|
||||
ErrorDialog(
|
||||
_('Not Trusted'),
|
||||
_('There was no trusted and active key found'))
|
||||
chat_control.sendmessage = False
|
||||
|
||||
def _encrypt_message(self, con, obj, callback):
|
||||
if not con.get_module('OpenPGP').secret_key_available:
|
||||
return
|
||||
con.get_module('OpenPGP').encrypt_message(obj, callback)
|
||||
|
||||
def _decrypt_message(self, con, obj, callback):
|
||||
if not con.get_module('OpenPGP').secret_key_available:
|
||||
return
|
||||
con.get_module('OpenPGP').decrypt_message(obj, callback)
|
||||
Reference in New Issue
Block a user