[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