[openpgp] Port to Gtk4

This commit is contained in:
Philipp Hörist
2025-02-08 16:10:31 +01:00
parent 3a5816259c
commit c67e7f341b
19 changed files with 935 additions and 1111 deletions

97
openpgp/backend/base.py Normal file
View File

@@ -0,0 +1,97 @@
# Copyright (C) 2025 Philipp Hörist <philipp AT hoerist.com>
#
# This file is part of the OpenPGP Gajim Plugin.
#
# OpenPGP Gajim Plugin is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# OpenPGP Gajim Plugin is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with OpenPGP Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
from typing import Any
from typing import TYPE_CHECKING
from collections.abc import Sequence
from pathlib import Path
from nbxmpp.protocol import JID
if TYPE_CHECKING:
from openpgp.modules.key_store import KeyData
class BaseKeyringItem:
def __init__(self, key: Any) -> None:
self._key = key
self._uid = self._get_uid()
@property
def is_xmpp_key(self) -> bool:
try:
return self.jid is not None
except Exception:
return False
def is_valid(self, jid: JID) -> bool:
if not self.is_xmpp_key:
return False
return jid == self.jid
def _get_uid(self) -> str | None:
raise NotImplementedError
@property
def fingerprint(self) -> str:
raise NotImplementedError
@property
def uid(self):
if self._uid is not None:
return self._uid
@property
def jid(self) -> JID | None:
if self._uid is not None:
return JID.from_string(self._uid)
def __hash__(self):
return hash(self.fingerprint)
class BasePGPBackend:
def __init__(self, jid: str, gnupghome: Path) -> None:
raise NotImplementedError
def generate_key(self) -> None:
raise NotImplementedError
def encrypt(
self, payload: bytes, keys: list[KeyData]
) -> tuple[bytes | None, str | None]:
raise NotImplementedError
def decrypt(self, payload: bytes) -> tuple[str, str]:
raise NotImplementedError
def get_keys(self) -> Sequence[BaseKeyringItem]:
raise NotImplementedError
def import_key(self, data: bytes, jid: JID) -> BaseKeyringItem | None:
raise NotImplementedError
def get_own_key_details(self) -> tuple[str | None, int | None]:
raise NotImplementedError
def export_key(self, fingerprint: str) -> bytes | None:
raise NotImplementedError
def delete_key(self, fingerprint: str) -> None:
raise NotImplementedError

View File

@@ -14,34 +14,29 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with OpenPGP Gajim Plugin. If not, see <http://www.gnu.org/licenses/>. # along with OpenPGP Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
from typing import Any
from typing import cast
import logging import logging
from collections.abc import Sequence
from pathlib import Path
import gpg import gpg
from gpg.errors import KeyNotFound
from gpg.results import ImportResult from gpg.results import ImportResult
from nbxmpp.protocol import JID from nbxmpp.protocol import JID
from openpgp.backend.base import BaseKeyringItem
from openpgp.backend.base import BasePGPBackend
from openpgp.backend.gpgme_types import Key
from openpgp.backend.util import parse_uid from openpgp.backend.util import parse_uid
from openpgp.modules.key_store import KeyData
from openpgp.modules.util import DecryptionFailed from openpgp.modules.util import DecryptionFailed
log = logging.getLogger("gajim.p.openpgp.gpgme") log = logging.getLogger("gajim.p.openpgp.gpgme")
class KeyringItem: class KeyringItem(BaseKeyringItem):
def __init__(self, key):
self._key = key
self._uid = self._get_uid()
@property
def is_xmpp_key(self) -> bool:
try:
return self.jid is not None
except Exception:
return False
def is_valid(self, jid: JID) -> bool:
if not self.is_xmpp_key:
return False
return jid == self.jid
def _get_uid(self) -> str | None: def _get_uid(self) -> str | None:
for uid in self._key.uids: for uid in self._key.uids:
@@ -51,36 +46,22 @@ class KeyringItem:
pass pass
@property @property
def fingerprint(self): def fingerprint(self) -> str:
return self._key.fpr return self._key.fpr
@property
def uid(self):
if self._uid is not None:
return self._uid
@property class GPGMe(BasePGPBackend):
def jid(self): def __init__(self, jid: str, gnuhome: Path) -> None:
if self._uid is not None:
return JID.from_string(self._uid)
def __hash__(self):
return hash(self.fingerprint)
class GPGME:
def __init__(self, jid, gnuhome):
self._jid = jid self._jid = jid
self._context_args = { self._home_dir = str(gnuhome)
"home_dir": str(gnuhome),
"offline": True,
"armor": False,
}
def generate_key(self): def _get_context(self) -> gpg.Context:
with gpg.Context(**self._context_args) as context: return gpg.Context(armor=False, offline=True, home_dir=self._home_dir)
def generate_key(self) -> None:
with self._get_context() as context:
result = context.create_key( result = context.create_key(
f"xmpp:{str(self._jid)}", f"xmpp:{self._jid}",
algorithm="default", algorithm="default",
expires=False, expires=False,
passphrase=None, passphrase=None,
@@ -89,11 +70,11 @@ class GPGME:
log.info("Generated new key: %s", result.fpr) log.info("Generated new key: %s", result.fpr)
def get_key(self, fingerprint): def _get_key(self, fingerprint: str) -> Key | None:
with gpg.Context(**self._context_args) as context: with self._get_context() as context:
try: try:
key = context.get_key(fingerprint) return cast(Key, context.get_key(fingerprint))
except gpg.errors.KeyNotFound as error: except KeyNotFound as error:
log.warning("key not found: %s", error.keystr) log.warning("key not found: %s", error.keystr)
return return
@@ -101,11 +82,9 @@ class GPGME:
log.warning("get_key() error: %s", error) log.warning("get_key() error: %s", error)
return return
return key def get_own_key_details(self) -> tuple[str | None, int | None]:
with self._get_context() as context:
def get_own_key_details(self): keys = cast(list[Key], list(context.keylist(secret=True)))
with gpg.Context(**self._context_args) as context:
keys = list(context.keylist(secret=True))
if not keys: if not keys:
return None, None return None, None
@@ -116,10 +95,10 @@ class GPGME:
return None, None return None, None
def get_keys(self): def get_keys(self) -> Sequence[KeyringItem]:
keys = [] keys: list[KeyringItem] = []
with gpg.Context(**self._context_args) as context: with self._get_context() as context:
for key in context.keylist(): for key in context.keylist(secret=False):
keyring_item = KeyringItem(key) keyring_item = KeyringItem(key)
if not keyring_item.is_xmpp_key: if not keyring_item.is_xmpp_key:
log.warning("Key not suited for xmpp: %s", key.fpr) log.warning("Key not suited for xmpp: %s", key.fpr)
@@ -130,10 +109,9 @@ class GPGME:
return keys return keys
def export_key(self, fingerprint): def export_key(self, fingerprint: str) -> bytes | None:
with gpg.Context(**self._context_args) as context: with self._get_context() as context:
key = context.key_export_minimal(pattern=fingerprint) return context.key_export_minimal(pattern=fingerprint)
return key
# def encrypt_decrypt_files(self): # def encrypt_decrypt_files(self):
# c = gpg.Context() # c = gpg.Context()
@@ -149,9 +127,11 @@ class GPGME:
# with open('foo2.txt', 'w') as output_file: # with open('foo2.txt', 'w') as output_file:
# c.decrypt(input_file, output_file) # c.decrypt(input_file, output_file)
def encrypt(self, plaintext, keys): def encrypt(
recipients = [] self, payload: bytes, keys: list[KeyData]
with gpg.Context(**self._context_args) as context: ) -> tuple[bytes | None, str | None]:
recipients: list[Any] = []
with self._get_context() as context:
for key in keys: for key in keys:
key = context.get_key(key.fingerprint) key = context.get_key(key.fingerprint)
if key is not None: if key is not None:
@@ -160,18 +140,18 @@ class GPGME:
if not recipients: if not recipients:
return None, "No keys found to encrypt to" return None, "No keys found to encrypt to"
with gpg.Context(**self._context_args) as context: with self._get_context() as context:
result = context.encrypt( result = context.encrypt(payload, recipients, always_trust=True)
str(plaintext).encode(), recipients, always_trust=True
)
ciphertext, result, _sign_result = result ciphertext, result, _sign_result = result
return ciphertext, None return ciphertext, None
def decrypt(self, ciphertext): raise RuntimeError
with gpg.Context(**self._context_args) as context:
def decrypt(self, payload: bytes) -> tuple[str, str]:
with self._get_context() as context:
try: try:
result = context.decrypt(ciphertext) result = context.decrypt(payload)
except Exception as error: except Exception as error:
raise DecryptionFailed("Decryption failed: %s" % error) raise DecryptionFailed("Decryption failed: %s" % error)
@@ -186,9 +166,9 @@ class GPGME:
return plaintext, fingerprints[0] return plaintext, fingerprints[0]
def import_key(self, data, jid): def import_key(self, data: bytes, jid: JID) -> KeyringItem | None:
log.info("Import key from %s", jid) log.info("Import key from %s", jid)
with gpg.Context(**self._context_args) as context: with self._get_context() as context:
result = context.key_import(data) result = context.key_import(data)
if not isinstance(result, ImportResult) or result.imported != 1: if not isinstance(result, ImportResult) or result.imported != 1:
log.error("Key import failed: %s", jid) log.error("Key import failed: %s", jid)
@@ -196,7 +176,7 @@ class GPGME:
return return
fingerprint = result.imports[0].fpr fingerprint = result.imports[0].fpr
key = self.get_key(fingerprint) key = self._get_key(fingerprint)
item = KeyringItem(key) item = KeyringItem(key)
if not item.is_valid(jid): if not item.is_valid(jid):
log.warning("Invalid key found") log.warning("Invalid key found")
@@ -206,8 +186,9 @@ class GPGME:
return item return item
def delete_key(self, fingerprint): def delete_key(self, fingerprint: str) -> None:
log.info("Delete Key: %s", fingerprint) log.info("Delete Key: %s", fingerprint)
key = self.get_key(fingerprint) key = self._get_key(fingerprint)
with gpg.Context(**self._context_args) as context: assert key is not None
with self._get_context() as context:
context.op_delete(key, True) context.op_delete(key, True)

View File

@@ -0,0 +1,86 @@
# Copyright (C) 2025 Philipp Hörist <philipp AT hoerist.com>
#
# This file is part of the OpenPGP Gajim Plugin.
#
# OpenPGP Gajim Plugin is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# OpenPGP Gajim Plugin is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with OpenPGP Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
from typing import Any
class UID:
address: Any | None
comment: str
email: str
invalid: int
last_update: int
name: str
origin: int
revoked: int
signatures: list[Any]
thisown: bool
tofu: list[Any]
uid: str
uidhash: str
validity: int
class SubKey:
can_authenticate: int
can_certify: int
can_encrypt: int
can_sign: int
card_number: Any | None
curve: Any | None
disabled: int
expired: int
expires: int
fpr: str
invalid: int
is_cardkey: int
is_de_vs: int
is_qualified: int
keygrip: Any | None
keyid: str
length: int
pubkey_algo: int
revoked: int
secret: int
thisown: bool
timestamp: int
class Key:
can_authenticate: int
can_certify: int
can_encrypt: int
can_sign: int
chain_id: Any | None
disabled: int
expired: int
fpr: str
invalid: int
is_qualified: int
issuer_name: str | None
issuer_serial: str | None
keylist_mode: int
last_update: int
origin: int
owner_trust: int
protocol: int
revoked: int
secret: int
subkeys: list[SubKey]
thisown: bool
uids: list[UID]

View File

@@ -15,12 +15,16 @@
# along with OpenPGP Gajim Plugin. If not, see <http://www.gnu.org/licenses/>. # along with OpenPGP Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
import logging import logging
from collections.abc import Sequence
from pathlib import Path from pathlib import Path
import gnupg import gnupg
from nbxmpp.protocol import JID from nbxmpp.protocol import JID
from openpgp.backend.base import BaseKeyringItem
from openpgp.backend.base import BasePGPBackend
from openpgp.backend.util import parse_uid from openpgp.backend.util import parse_uid
from openpgp.modules.key_store import KeyData
from openpgp.modules.util import DecryptionFailed from openpgp.modules.util import DecryptionFailed
log = logging.getLogger("gajim.p.openpgp.pygnupg") log = logging.getLogger("gajim.p.openpgp.pygnupg")
@@ -30,22 +34,7 @@ if log.getEffectiveLevel() == logging.DEBUG:
log.setLevel(logging.DEBUG) log.setLevel(logging.DEBUG)
class KeyringItem: class KeyringItem(BaseKeyringItem):
def __init__(self, key):
self._key = key
self._uid = self._get_uid()
@property
def is_xmpp_key(self) -> bool:
try:
return self.jid is not None
except Exception:
return False
def is_valid(self, jid: JID) -> bool:
if not self.is_xmpp_key:
return False
return jid == self.jid
@property @property
def keyid(self) -> str: def keyid(self) -> str:
@@ -59,32 +48,18 @@ class KeyringItem:
pass pass
@property @property
def fingerprint(self): def fingerprint(self) -> str:
return self._key["fingerprint"] return self._key["fingerprint"]
@property
def uid(self):
if self._uid is not None:
return self._uid
@property class PythonGnuPG(BasePGPBackend):
def jid(self):
if self._uid is not None:
return JID.from_string(self._uid)
def __hash__(self):
return hash(self.fingerprint)
class PythonGnuPG(gnupg.GPG):
def __init__(self, jid: str, gnupghome: Path) -> None: def __init__(self, jid: str, gnupghome: Path) -> None:
gnupg.GPG.__init__(self, gpgbinary="gpg", gnupghome=str(gnupghome)) self._gnupg = gnupg.GPG(gpgbinary="gpg", gnupghome=str(gnupghome))
self._jid = jid self._jid = jid
self._own_fingerprint = None self._own_fingerprint = None
@staticmethod @staticmethod
def _get_key_params(jid): def _get_key_params(jid: str) -> str:
""" """
Generate --gen-key input Generate --gen-key input
""" """
@@ -102,17 +77,19 @@ class PythonGnuPG(gnupg.GPG):
out += "%commit\n" out += "%commit\n"
return out return out
def generate_key(self): def generate_key(self) -> None:
super().gen_key(self._get_key_params(self._jid)) self._gnupg.gen_key(self._get_key_params(self._jid))
def encrypt(self, payload, keys): def encrypt(
self, payload: bytes, keys: list[KeyData]
) -> tuple[bytes | None, str | None]:
recipients = [key.fingerprint for key in keys] recipients = [key.fingerprint for key in keys]
log.info("encrypt to:") log.info("encrypt to:")
for fingerprint in recipients: for fingerprint in recipients:
log.info(fingerprint) log.info(fingerprint)
result = super().encrypt( result = self._gnupg.encrypt(
str(payload).encode("utf8"), payload,
recipients, recipients,
armor=False, armor=False,
sign=self._own_fingerprint, sign=self._own_fingerprint,
@@ -126,19 +103,20 @@ class PythonGnuPG(gnupg.GPG):
return result.data, error return result.data, error
def decrypt(self, payload): def decrypt(self, payload: bytes) -> tuple[str, str]:
result = super().decrypt(payload, always_trust=True) result = self._gnupg.decrypt(payload, always_trust=True)
if not result.ok: if not result.ok:
raise DecryptionFailed(result.status) raise DecryptionFailed(result.status)
assert result.fingerprint is not None
return result.data.decode("utf8"), result.fingerprint return result.data.decode("utf8"), result.fingerprint
def get_key(self, fingerprint): def _get_key(self, fingerprint: str) -> gnupg.ListKeys:
return super().list_keys(keys=[fingerprint]) return self._gnupg.list_keys(keys=[fingerprint])
def get_keys(self, secret=False): def get_keys(self) -> Sequence[KeyringItem]:
result = super().list_keys(secret=secret) result = self._gnupg.list_keys(secret=False)
keys = [] keys: list[KeyringItem] = []
for key in result: for key in result:
item = KeyringItem(key) item = KeyringItem(key)
if not item.is_xmpp_key: if not item.is_xmpp_key:
@@ -149,15 +127,18 @@ class PythonGnuPG(gnupg.GPG):
keys.append(item) keys.append(item)
return keys return keys
def import_key(self, data, jid): def import_key(self, data: bytes, jid: JID) -> KeyringItem | None:
log.info("Import key from %s", jid) log.info("Import key from %s", jid)
result = super().import_keys(data) result = self._gnupg.import_keys(data)
if not result: if not result:
log.error("Could not import key") log.error("Could not import key")
log.error(result) log.error(result)
return return
key = self.get_key(result.results[0]["fingerprint"]) fpr = result.results[0]["fingerprint"]
assert fpr is not None
key = self._get_key(fpr)
item = KeyringItem(key[0]) item = KeyringItem(key[0])
if not item.is_valid(jid): if not item.is_valid(jid):
log.warning("Invalid key found, deleting key") log.warning("Invalid key found, deleting key")
@@ -167,8 +148,8 @@ class PythonGnuPG(gnupg.GPG):
return item return item
def get_own_key_details(self): def get_own_key_details(self) -> tuple[str | None, int | None]:
result = super().list_keys(secret=True) result = self._gnupg.list_keys(secret=True)
if not result: if not result:
return None, None return None, None
@@ -179,10 +160,13 @@ class PythonGnuPG(gnupg.GPG):
self._own_fingerprint = result[0]["fingerprint"] self._own_fingerprint = result[0]["fingerprint"]
return self._own_fingerprint, int(result[0]["date"]) return self._own_fingerprint, int(result[0]["date"])
def export_key(self, fingerprint): def export_key(self, fingerprint: str) -> bytes | None:
key = super().export_keys(fingerprint, secret=False, armor=False, minimal=True) key = self._gnupg.export_keys(
fingerprint, secret=False, armor=False, minimal=True
)
assert isinstance(key, bytes | None)
return key return key
def delete_key(self, fingerprint): def delete_key(self, fingerprint: str) -> None:
log.info("Delete Key: %s", fingerprint) log.info("Delete Key: %s", fingerprint)
super().delete_keys(fingerprint) self._gnupg.delete_keys(fingerprint)

View File

@@ -14,9 +14,18 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with OpenPGP Gajim Plugin. If not, see <http://www.gnu.org/licenses/>. # along with OpenPGP Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
from typing import Any
from typing import NamedTuple
import logging import logging
import sqlite3 import sqlite3
from collections import namedtuple from collections import namedtuple
from collections.abc import Iterator
from pathlib import Path
from nbxmpp.protocol import JID
from openpgp.modules.util import Trust
log = logging.getLogger("gajim.p.openpgp.sql") log = logging.getLogger("gajim.p.openpgp.sql")
@@ -32,8 +41,16 @@ TABLE_LAYOUT = """
CREATE UNIQUE INDEX jid_fingerprint ON contacts (jid, fingerprint);""" CREATE UNIQUE INDEX jid_fingerprint ON contacts (jid, fingerprint);"""
class ContactRow(NamedTuple):
jid: JID
fingerprint: str
active: bool
trust: Trust
timestamp: float
class Storage: class Storage:
def __init__(self, folder_path): def __init__(self, folder_path: Path) -> None:
self._con = sqlite3.connect( self._con = sqlite3.connect(
str(folder_path / "contacts.db"), detect_types=sqlite3.PARSE_COLNAMES str(folder_path / "contacts.db"), detect_types=sqlite3.PARSE_COLNAMES
) )
@@ -45,21 +62,21 @@ class Storage:
self._con.commit() self._con.commit()
@staticmethod @staticmethod
def _namedtuple_factory(cursor, row): def _namedtuple_factory(cursor: sqlite3.Cursor, row: Any) -> Any:
fields = [col[0] for col in cursor.description] fields = [col[0] for col in cursor.description]
Row = namedtuple("Row", fields) Row = namedtuple("Row", fields) # pyright: ignore
named_row = Row(*row) named_row = Row(*row)
return named_row return named_row
def _user_version(self): def _user_version(self) -> int:
return self._con.execute("PRAGMA user_version").fetchone()[0] return self._con.execute("PRAGMA user_version").fetchone()[0]
def _create_database(self): def _create_database(self) -> None:
if not self._user_version(): if not self._user_version():
log.info("Create contacts.db") log.info("Create contacts.db")
self._execute_query(TABLE_LAYOUT) self._execute_query(TABLE_LAYOUT)
def _execute_query(self, query): def _execute_query(self, query: str) -> None:
transaction = """ transaction = """
BEGIN TRANSACTION; BEGIN TRANSACTION;
%s %s
@@ -70,40 +87,41 @@ class Storage:
) )
self._con.executescript(transaction) self._con.executescript(transaction)
def _migrate_database(self): def _migrate_database(self) -> None:
pass pass
def load_contacts(self): def load_contacts(self) -> list[ContactRow]:
sql = """SELECT jid as "jid [jid]", sql = """SELECT jid as "jid [jid]",
fingerprint, fingerprint,
active, active,
trust, trust,
timestamp, timestamp
comment
FROM contacts""" FROM contacts"""
return self._con.execute(sql).fetchall() return self._con.execute(sql).fetchall()
def save_contact(self, db_values): def save_contact(
self, db_values: Iterator[tuple[JID, str, bool, Trust, float]]
) -> None:
sql = """REPLACE INTO sql = """REPLACE INTO
contacts(jid, fingerprint, active, trust, timestamp, comment) contacts(jid, fingerprint, active, trust, timestamp)
VALUES(?, ?, ?, ?, ?, ?)""" VALUES(?, ?, ?, ?, ?, ?)"""
for values in db_values: for values in db_values:
log.info("Store key: %s", values) log.info("Store key: %s", values)
self._con.execute(sql, values) self._con.execute(sql, values)
self._con.commit() self._con.commit()
def set_trust(self, jid, fingerprint, trust): def set_trust(self, jid: JID, fingerprint: str, trust: Trust) -> None:
sql = "UPDATE contacts SET trust = ? WHERE jid = ? AND fingerprint = ?" sql = "UPDATE contacts SET trust = ? WHERE jid = ? AND fingerprint = ?"
log.info("Set Trust: %s %s %s", trust, jid, fingerprint) log.info("Set Trust: %s %s %s", trust, jid, fingerprint)
self._con.execute(sql, (trust, jid, fingerprint)) self._con.execute(sql, (trust, jid, fingerprint))
self._con.commit() self._con.commit()
def delete_key(self, jid, fingerprint): def delete_key(self, jid: JID, fingerprint: str) -> None:
sql = "DELETE from contacts WHERE jid = ? AND fingerprint = ?" sql = "DELETE from contacts WHERE jid = ? AND fingerprint = ?"
log.info("Delete Key: %s %s", jid, fingerprint) log.info("Delete Key: %s %s", jid, fingerprint)
self._con.execute(sql, (jid, fingerprint)) self._con.execute(sql, (jid, fingerprint))
self._con.commit() self._con.commit()
def cleanup(self): def cleanup(self) -> None:
self._con.close() self._con.close()

View File

@@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import annotations
def parse_uid(uid: str, compat=False) -> str: def parse_uid(uid: str, compat: bool = False) -> str:
if uid.startswith("xmpp:"): if uid.startswith("xmpp:"):
return uid[5:] return uid[5:]

View File

@@ -14,6 +14,8 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with OpenPGP Gajim Plugin. If not, see <http://www.gnu.org/licenses/>. # along with OpenPGP Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
import logging import logging
import time import time
@@ -22,8 +24,11 @@ from gi.repository import Gtk
from gajim.common import app from gajim.common import app
from gajim.gtk.dialogs import ConfirmationDialog from gajim.gtk.dialogs import ConfirmationDialog
from gajim.gtk.dialogs import DialogButton from gajim.gtk.dialogs import DialogButton
from gajim.gtk.util.misc import container_remove_all
from gajim.gtk.widgets import GajimAppWindow
from gajim.plugins.plugins_i18n import _ from gajim.plugins.plugins_i18n import _
from openpgp.modules.key_store import KeyData
from openpgp.modules.util import Trust from openpgp.modules.util import Trust
log = logging.getLogger("gajim.p.openpgp.keydialog") log = logging.getLogger("gajim.p.openpgp.keydialog")
@@ -36,48 +41,60 @@ TRUST_DATA = {
} }
class KeyDialog(Gtk.Dialog): class KeyDialog(GajimAppWindow):
def __init__(self, account, jid, transient): def __init__(self, account: str, jid: str, transient: Gtk.Window) -> None:
super().__init__(title=_("Public Keys for %s") % jid, destroy_with_parent=True)
self.set_transient_for(transient) GajimAppWindow.__init__(
self.set_resizable(True) self,
self.set_default_size(500, 300) name="PGPKeyDialog",
title=_("Public Keys for %s") % jid,
default_width=450,
default_height=400,
transient_for=transient,
modal=True,
)
self.get_style_context().add_class("openpgp-key-dialog") self.window.add_css_class("openpgp-key-dialog")
self._client = app.get_client(account) self._client = app.get_client(account)
self._listbox = Gtk.ListBox() self._listbox = Gtk.ListBox()
self._listbox.set_selection_mode(Gtk.SelectionMode.NONE) self._listbox.set_selection_mode(Gtk.SelectionMode.NONE)
self._scrolled = Gtk.ScrolledWindow() self._scrolled = Gtk.ScrolledWindow(hexpand=True)
self._scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) self._scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
self._scrolled.add(self._listbox) self._scrolled.set_child(self._listbox)
box = self.get_content_area() self.set_child(self._scrolled)
box.pack_start(self._scrolled, True, True, 0)
keys = self._client.get_module("OpenPGP").get_keys(jid, only_trusted=False) keys = self._client.get_module("OpenPGP").get_keys(jid, only_trusted=False)
for key in keys: for key in keys:
log.info("Load: %s", key.fingerprint) log.info("Load: %s", key.fingerprint)
self._listbox.add(KeyRow(key)) self._listbox.append(KeyRow(key, self))
self.show_all()
self.show()
def _cleanup(self) -> None:
del self._client
del self._listbox
del self._scrolled
class KeyRow(Gtk.ListBoxRow): class KeyRow(Gtk.ListBoxRow):
def __init__(self, key): def __init__(self, key: KeyData, dialog: GajimAppWindow):
Gtk.ListBoxRow.__init__(self) Gtk.ListBoxRow.__init__(self)
self.set_activatable(False) self.set_activatable(False)
self._dialog = self.get_toplevel() self._dialog = dialog
self.key = key self.key = key
box = Gtk.Box() box = Gtk.Box()
box.set_spacing(12) box.set_spacing(12)
self._trust_button = TrustButton(self) self._trust_button = Gtk.MenuButton()
box.add(self._trust_button) self._trust_button.set_popover(TrustPopver(self))
self._update_button_state()
box.append(self._trust_button)
label_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) label_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
fingerprint = Gtk.Label(label=self._format_fingerprint(key.fingerprint)) fingerprint = Gtk.Label(label=self._format_fingerprint(key.fingerprint))
@@ -88,25 +105,37 @@ class KeyRow(Gtk.ListBoxRow):
fingerprint.set_halign(Gtk.Align.START) fingerprint.set_halign(Gtk.Align.START)
fingerprint.set_valign(Gtk.Align.START) fingerprint.set_valign(Gtk.Align.START)
fingerprint.set_hexpand(True) fingerprint.set_hexpand(True)
label_box.add(fingerprint) label_box.append(fingerprint)
date = Gtk.Label(label=self._format_timestamp(key.timestamp)) date = Gtk.Label(label=self._format_timestamp(key.timestamp))
date.set_halign(Gtk.Align.START) date.set_halign(Gtk.Align.START)
date.get_style_context().add_class("openpgp-mono") date.get_style_context().add_class("openpgp-mono")
if not key.active: if not key.active:
date.get_style_context().add_class("openpgp-inactive-color") date.get_style_context().add_class("openpgp-inactive-color")
label_box.add(date) label_box.append(date)
box.add(label_box) box.append(label_box)
self.set_child(box)
self.add(box) def _update_button_state(self) -> None:
self.show_all() icon_name, tooltip, css_class = TRUST_DATA[self.key.trust]
self._trust_button.set_icon_name(icon_name)
def delete_fingerprint(self, *args): for css_cls in self._trust_button.get_css_classes():
if css_cls.startswith("openpgp"):
self._trust_button.remove_css_class(css_cls)
if not self.key.active:
css_class = "inactive-color"
tooltip = "%s - %s" % (_("Inactive"), tooltip)
self._trust_button.add_css_class(f"openpgp-{css_class}")
self._trust_button.set_tooltip_text(tooltip)
def delete_fingerprint(self):
def _remove(): def _remove():
self.get_parent().remove(self) self.get_parent().remove(self)
self.key.delete() self.key.delete()
self.destroy()
ConfirmationDialog( ConfirmationDialog(
_("Delete Public Key?"), _("Delete Public Key?"),
@@ -117,98 +146,76 @@ class KeyRow(Gtk.ListBoxRow):
], ],
).show() ).show()
def set_trust(self, trust): def set_trust(self, trust: Trust) -> None:
icon_name, tooltip, css_class = TRUST_DATA[trust] self.key.trust = trust
image = self._trust_button.get_child() self._update_button_state()
image.set_from_icon_name(icon_name, Gtk.IconSize.MENU)
image.get_style_context().add_class(css_class)
@staticmethod @staticmethod
def _format_fingerprint(fingerprint): def _format_fingerprint(fingerprint: str) -> str:
fplen = len(fingerprint) fplen = len(fingerprint)
wordsize = fplen // 8 wordsize = fplen // 8
buf = "" buf = ""
for w in range(0, fplen, wordsize): for w in range(0, fplen, wordsize):
buf += "{0} ".format(fingerprint[w : w + wordsize]) buf += f"{fingerprint[w : w + wordsize]} "
return buf.rstrip() return buf.rstrip()
@staticmethod @staticmethod
def _format_timestamp(timestamp): def _format_timestamp(timestamp: float) -> str:
return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(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): class TrustPopver(Gtk.Popover):
def __init__(self, row): def __init__(self, row: KeyRow):
Gtk.Popover.__init__(self) Gtk.Popover.__init__(self)
self._row = row self._row = row
self._listbox = Gtk.ListBox() self._listbox = Gtk.ListBox()
self._listbox.set_selection_mode(Gtk.SelectionMode.NONE) self._listbox.set_selection_mode(Gtk.SelectionMode.NONE)
if row.key.trust != Trust.VERIFIED: if row.key.trust != Trust.VERIFIED:
self._listbox.add(VerifiedOption()) self._listbox.append(VerifiedOption())
if row.key.trust != Trust.NOT_TRUSTED: if row.key.trust != Trust.NOT_TRUSTED:
self._listbox.add(NotTrustedOption()) self._listbox.append(NotTrustedOption())
self._listbox.add(DeleteOption()) self._listbox.append(DeleteOption())
self.add(self._listbox) self.set_child(self._listbox)
self._listbox.show_all()
self._listbox.connect("row-activated", self._activated) self._listbox.connect("row-activated", self._activated)
self.get_style_context().add_class("openpgp-trust-popover") self.add_css_class("openpgp-trust-popover")
def _activated(self, listbox, row): def _activated(self, listbox: Gtk.ListBox, row: MenuOption) -> None:
self.popdown() self.popdown()
if row.type_ is None: if row.type_ is None:
self._row.delete_fingerprint() self._row.delete_fingerprint()
else: else:
self._row.key.trust = row.type_ self._row.set_trust(row.type_)
self.get_relative_to().update()
self.update() self.update()
def update(self): def update(self):
self._listbox.foreach(lambda row: self._listbox.remove(row)) container_remove_all(self._listbox)
if self._row.key.trust != Trust.VERIFIED: if self._row.key.trust != Trust.VERIFIED:
self._listbox.add(VerifiedOption()) self._listbox.append(VerifiedOption())
if self._row.key.trust != Trust.NOT_TRUSTED: if self._row.key.trust != Trust.NOT_TRUSTED:
self._listbox.add(NotTrustedOption()) self._listbox.append(NotTrustedOption())
self._listbox.add(DeleteOption()) self._listbox.append(DeleteOption())
class MenuOption(Gtk.ListBoxRow): class MenuOption(Gtk.ListBoxRow):
type_: Trust | None
icon: str
label: str
color: str
def __init__(self): def __init__(self):
Gtk.ListBoxRow.__init__(self) Gtk.ListBoxRow.__init__(self)
box = Gtk.Box() box = Gtk.Box()
box.set_spacing(6) box.set_spacing(6)
image = Gtk.Image.new_from_icon_name(self.icon, Gtk.IconSize.MENU) image = Gtk.Image.new_from_icon_name(self.icon)
label = Gtk.Label(label=self.label) if self.color:
image.get_style_context().add_class(self.color) image.add_css_class(self.color)
box.add(image) label = Gtk.Label(label=self.label)
box.add(label) box.append(image)
self.add(box) box.append(label)
self.show_all() self.set_child(box)
class VerifiedOption(MenuOption): class VerifiedOption(MenuOption):

View File

@@ -1,16 +1,14 @@
.openpgp-inactive-color { color: @unfocused_borders; } .openpgp-inactive-color button > box > image { color: @unfocused_borders; }
.openpgp-error-color button > box > image { color: @error_color; }
.openpgp-warning-color button > box > image { color: @warning_color; }
.openpgp-encrypted-color button > box > image { color: rgb(75, 181, 67); }
.openpgp-mono { font-size: 12px; font-family: monospace; } .openpgp-mono { font-size: 12px; font-family: monospace; }
.openpgp-key-dialog > box { margin: 12px; } .openpgp-key-dialog > box { margin: 12px; }
.openpgp-key-dialog scrolledwindow row { .openpgp-key-dialog scrolledwindow row {
border-bottom: 1px solid; border-bottom: 1px solid;
border-color: @unfocused_borders; border-color: @unfocused_borders;
padding: 10px 20px 10px 10px; padding: 10px 20px 10px 10px;
} }
.openpgp-key-dialog scrolledwindow row:last-child { border-bottom: 0px} .openpgp-key-dialog scrolledwindow row:last-child { border-bottom: 0px; }
.openpgp-key-dialog scrolledwindow { border: 1px solid; border-color:@unfocused_borders; } .openpgp-key-dialog scrolledwindow { border: 1px solid; border-color:@unfocused_borders; }
.openpgp-trust-popover row { padding: 10px 15px 10px 10px; } .openpgp-trust-popover row { padding: 10px 15px 10px 10px; }

View File

@@ -14,6 +14,8 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with OpenPGP Gajim Plugin. If not, see <http://www.gnu.org/licenses/>. # along with OpenPGP Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
from typing import cast
import logging import logging
import threading import threading
from enum import IntEnum from enum import IntEnum
@@ -22,8 +24,12 @@ from gi.repository import GLib
from gi.repository import Gtk from gi.repository import Gtk
from gajim.common import app from gajim.common import app
from gajim.common.client import Client
from gajim.gtk.control import ChatControl
from gajim.plugins.plugins_i18n import _ from gajim.plugins.plugins_i18n import _
from ..pgpplugin import OpenPGPPlugin
log = logging.getLogger("gajim.p.openpgp.wizard") log = logging.getLogger("gajim.p.openpgp.wizard")
@@ -35,7 +41,9 @@ class Page(IntEnum):
class KeyWizard(Gtk.Assistant): class KeyWizard(Gtk.Assistant):
def __init__(self, plugin, account, chat_control): def __init__(
self, plugin: OpenPGPPlugin, account: str, chat_control: ChatControl
) -> None:
Gtk.Assistant.__init__(self) Gtk.Assistant.__init__(self)
self._client = app.get_client(account) self._client = app.get_client(account)
@@ -48,7 +56,6 @@ class KeyWizard(Gtk.Assistant):
self.set_application(app.app) self.set_application(app.app)
self.set_transient_for(app.window) self.set_transient_for(app.window)
self.set_resizable(True) self.set_resizable(True)
self.set_position(Gtk.WindowPosition.CENTER)
self.set_default_size(600, 400) self.set_default_size(600, 400)
self.get_style_context().add_class("dialog-margin") self.get_style_context().add_class("dialog-margin")
@@ -65,9 +72,9 @@ class KeyWizard(Gtk.Assistant):
self.connect("close", self._on_cancel) self.connect("close", self._on_cancel)
self._remove_sidebar() self._remove_sidebar()
self.show_all() self.show()
def _add_page(self, page): def _add_page(self, page: Gtk.Box) -> None:
self.append_page(page) self.append_page(page)
self.set_page_type(page, page.type_) self.set_page_type(page, page.type_)
self.set_page_title(page, page.title) self.set_page_title(page, page.title)
@@ -82,7 +89,7 @@ class KeyWizard(Gtk.Assistant):
action = app.window.lookup_action("set-encryption") action = app.window.lookup_action("set-encryption")
action.activate(GLib.Variant("s", self._plugin.encryption_name)) action.activate(GLib.Variant("s", self._plugin.encryption_name))
def _on_page_change(self, assistant, page): def _on_page_change(self, assistant: Gtk.Assistant, page: Page) -> None:
if self.get_current_page() == Page.NEWKEY: if self.get_current_page() == Page.NEWKEY:
if self._client.get_module("OpenPGP").secret_key_available: if self._client.get_module("OpenPGP").secret_key_available:
self.set_current_page(Page.SUCCESS) self.set_current_page(Page.SUCCESS)
@@ -91,8 +98,8 @@ class KeyWizard(Gtk.Assistant):
elif self.get_current_page() == Page.SUCCESS: elif self.get_current_page() == Page.SUCCESS:
self._activate_encryption() self._activate_encryption()
def _on_cancel(self, widget): def _on_cancel(self, widget: Gtk.Assistant):
self.destroy() self.close()
class WelcomePage(Gtk.Box): class WelcomePage(Gtk.Box):
@@ -106,8 +113,8 @@ class WelcomePage(Gtk.Box):
self.set_spacing(18) self.set_spacing(18)
title_label = Gtk.Label(label=_("Setup OpenPGP")) title_label = Gtk.Label(label=_("Setup OpenPGP"))
text_label = Gtk.Label(label=_("Gajim will now try to setup OpenPGP for you")) text_label = Gtk.Label(label=_("Gajim will now try to setup OpenPGP for you"))
self.add(title_label) self.append(title_label)
self.add(text_label) self.append(text_label)
class RequestPage(Gtk.Box): class RequestPage(Gtk.Box):
@@ -120,7 +127,7 @@ class RequestPage(Gtk.Box):
super().__init__(orientation=Gtk.Orientation.VERTICAL) super().__init__(orientation=Gtk.Orientation.VERTICAL)
self.set_spacing(18) self.set_spacing(18)
spinner = Gtk.Spinner() spinner = Gtk.Spinner()
self.pack_start(spinner, True, True, 0) self.append(spinner)
spinner.start() spinner.start()
@@ -148,7 +155,7 @@ class NewKeyPage(RequestPage):
title = _("Generating new Key") title = _("Generating new Key")
complete = False complete = False
def __init__(self, assistant, client): def __init__(self, assistant: Gtk.Assistant, client: Client) -> None:
super().__init__() super().__init__()
self._assistant = assistant self._assistant = assistant
self._client = client self._client = client
@@ -167,14 +174,14 @@ class NewKeyPage(RequestPage):
GLib.idle_add(self.finished, text) GLib.idle_add(self.finished, text)
def finished(self, error): def finished(self, error: str | None) -> None:
if error is None: if error is None:
self._client.get_module("OpenPGP").get_own_key_details() self._client.get_module("OpenPGP").get_own_key_details()
self._client.get_module("OpenPGP").set_public_key() self._client.get_module("OpenPGP").set_public_key()
self._client.get_module("OpenPGP").request_keylist() self._client.get_module("OpenPGP").request_keylist()
self._assistant.set_current_page(Page.SUCCESS) self._assistant.set_current_page(Page.SUCCESS)
else: else:
error_page = self._assistant.get_nth_page(Page.ERROR) error_page = cast(ErrorPage, self._assistant.get_nth_page(Page.ERROR))
error_page.set_text(error) error_page.set_text(error)
self._assistant.set_current_page(Page.ERROR) self._assistant.set_current_page(Page.ERROR)
@@ -206,17 +213,15 @@ class SuccessfulPage(Gtk.Box):
self.set_spacing(12) self.set_spacing(12)
self.set_homogeneous(True) self.set_homogeneous(True)
icon = Gtk.Image.new_from_icon_name( icon = Gtk.Image.new_from_icon_name("object-select-symbolic")
"object-select-symbolic", Gtk.IconSize.DIALOG icon.add_css_class("success-color")
)
icon.get_style_context().add_class("success-color")
icon.set_valign(Gtk.Align.END) icon.set_valign(Gtk.Align.END)
label = Gtk.Label(label=_("Setup successful")) label = Gtk.Label(label=_("Setup successful"))
label.get_style_context().add_class("bold16") label.add_css_class("bold16")
label.set_valign(Gtk.Align.START) label.set_valign(Gtk.Align.START)
self.add(icon) self.append(icon)
self.add(label) self.append(label)
class ErrorPage(Gtk.Box): class ErrorPage(Gtk.Box):
@@ -230,17 +235,15 @@ class ErrorPage(Gtk.Box):
self.set_spacing(12) self.set_spacing(12)
self.set_homogeneous(True) self.set_homogeneous(True)
icon = Gtk.Image.new_from_icon_name( icon = Gtk.Image.new_from_icon_name("dialog-error-symbolic")
"dialog-error-symbolic", Gtk.IconSize.DIALOG
)
icon.get_style_context().add_class("error-color") icon.get_style_context().add_class("error-color")
icon.set_valign(Gtk.Align.END) icon.set_valign(Gtk.Align.END)
self._label = Gtk.Label() self._label = Gtk.Label()
self._label.get_style_context().add_class("bold16") self._label.get_style_context().add_class("bold16")
self._label.set_valign(Gtk.Align.START) self._label.set_valign(Gtk.Align.START)
self.add(icon) self.append(icon)
self.add(self._label) self.append(self._label)
def set_text(self, text): def set_text(self, text: str) -> None:
self._label.set_text(text) self._label.set_text(text)

View File

@@ -14,8 +14,17 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with OpenPGP Gajim Plugin. If not, see <http://www.gnu.org/licenses/>. # along with OpenPGP Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
import logging from __future__ import annotations
import logging
from collections.abc import Iterator
from nbxmpp.protocol import JID
from nbxmpp.structs import PGPKeyMetadata
from openpgp.backend.base import BasePGPBackend
from openpgp.backend.sql import ContactRow
from openpgp.backend.sql import Storage
from openpgp.modules.util import Trust from openpgp.modules.util import Trust
log = logging.getLogger("gajim.p.openpgp.store") log = logging.getLogger("gajim.p.openpgp.store")
@@ -26,45 +35,34 @@ class KeyData:
Holds all data related to a certain key Holds all data related to a certain key
""" """
def __init__(self, contact_data): def __init__(
self,
contact_data: ContactData,
fingerprint: str,
active: bool,
trust: Trust,
timestamp: float,
):
self._contact_data = contact_data self._contact_data = contact_data
self.fingerprint = None self.fingerprint = fingerprint
self.active = False self.active = active
self._trust = Trust.UNKNOWN self._trust = trust
self.timestamp = None self.timestamp = timestamp
self.comment = None self.comment = None
self.has_pubkey = False self.has_pubkey = False
@property @property
def trust(self): def trust(self) -> Trust:
return self._trust return self._trust
@trust.setter @trust.setter
def trust(self, value): def trust(self, value: Trust) -> None:
if value not in (Trust.NOT_TRUSTED, Trust.UNKNOWN, Trust.BLIND, Trust.VERIFIED): if value not in (Trust.NOT_TRUSTED, Trust.UNKNOWN, Trust.BLIND, Trust.VERIFIED):
raise ValueError("Trust value not allowed: %s" % value) raise ValueError("Trust value not allowed: %s" % value)
self._trust = value self._trust = value
self._contact_data.set_trust(self.fingerprint, self._trust) 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): def delete(self):
self._contact_data.delete_key(self.fingerprint) self._contact_data.delete_key(self.fingerprint)
@@ -74,9 +72,9 @@ class ContactData:
Holds all data related to a contact Holds all data related to a contact
""" """
def __init__(self, jid, storage, pgp): def __init__(self, jid: JID, storage: Storage, pgp: BasePGPBackend) -> None:
self.jid = jid self.jid = jid
self._key_store = {} self._key_store: dict[str, KeyData] = {}
self._storage = storage self._storage = storage
self._pgp = pgp self._pgp = pgp
@@ -87,13 +85,13 @@ class ContactData:
return "xmpp:%s" % self.jid return "xmpp:%s" % self.jid
@property @property
def default_trust(self): def default_trust(self) -> Trust:
for key in self._key_store.values(): for key in self._key_store.values():
if key.trust in (Trust.NOT_TRUSTED, Trust.BLIND): if key.trust in (Trust.NOT_TRUSTED, Trust.BLIND):
return Trust.UNKNOWN return Trust.UNKNOWN
return Trust.BLIND return Trust.BLIND
def db_values(self): def db_values(self) -> Iterator[tuple[JID, str, bool, Trust, float]]:
for key in self._key_store.values(): for key in self._key_store.values():
yield ( yield (
self.jid, self.jid,
@@ -101,28 +99,39 @@ class ContactData:
key.active, key.active,
key.trust, key.trust,
key.timestamp, key.timestamp,
key.comment,
) )
def add_from_key(self, key): def add_from_key(self, key: PGPKeyMetadata) -> KeyData:
try: try:
keydata = self._key_store[key.fingerprint] keydata = self._key_store[key.fingerprint]
except KeyError: except KeyError:
keydata = KeyData.from_key(self, key, self.default_trust) keydata = KeyData(
self,
key.fingerprint,
True,
self.default_trust,
key.date,
)
self._key_store[key.fingerprint] = keydata self._key_store[key.fingerprint] = keydata
log.info("Add from key: %s %s", self.jid, keydata.fingerprint) log.info("Add from key: %s %s", self.jid, keydata.fingerprint)
return keydata return keydata
def add_from_db(self, row): def add_from_db(self, row: ContactRow) -> KeyData:
try: try:
keydata = self._key_store[row.fingerprint] keydata = self._key_store[row.fingerprint]
except KeyError: except KeyError:
keydata = KeyData.from_row(self, row) keydata = KeyData(
self,
row.fingerprint,
row.active,
row.trust,
row.timestamp,
)
self._key_store[row.fingerprint] = keydata self._key_store[row.fingerprint] = keydata
log.info("Add from row: %s %s", self.jid, row.fingerprint) log.info("Add from row: %s %s", self.jid, row.fingerprint)
return keydata return keydata
def process_keylist(self, keylist): def process_keylist(self, keylist: list[PGPKeyMetadata] | None) -> list[str]:
log.info("Process keylist: %s %s", self.jid, keylist) log.info("Process keylist: %s %s", self.jid, keylist)
if keylist is None: if keylist is None:
@@ -131,8 +140,8 @@ class ContactData:
self._storage.save_contact(self.db_values()) self._storage.save_contact(self.db_values())
return [] return []
missing_pub_keys = [] missing_pub_keys: list[str] = []
fingerprints = set([key.fingerprint for key in keylist]) fingerprints = {key.fingerprint for key in keylist}
if fingerprints == self._key_store.keys(): if fingerprints == self._key_store.keys():
log.info("No updates found") log.info("No updates found")
for key in self._key_store.values(): for key in self._key_store.values():
@@ -156,7 +165,7 @@ class ContactData:
self._storage.save_contact(self.db_values()) self._storage.save_contact(self.db_values())
return missing_pub_keys return missing_pub_keys
def set_public_key(self, fingerprint): def set_public_key(self, fingerprint: str) -> None:
try: try:
keydata = self._key_store[fingerprint] keydata = self._key_store[fingerprint]
except KeyError: except KeyError:
@@ -167,7 +176,7 @@ class ContactData:
keydata.has_pubkey = True keydata.has_pubkey = True
log.info("Set public key: %s %s", self.jid, fingerprint) log.info("Set public key: %s %s", self.jid, fingerprint)
def get_keys(self, only_trusted=True): def get_keys(self, only_trusted: bool = True) -> list[KeyData]:
keys = list(self._key_store.values()) keys = list(self._key_store.values())
if not only_trusted: if not only_trusted:
return keys return keys
@@ -175,13 +184,13 @@ class ContactData:
k for k in keys if k.active and k.trust in (Trust.VERIFIED, Trust.BLIND) k for k in keys if k.active and k.trust in (Trust.VERIFIED, Trust.BLIND)
] ]
def get_key(self, fingerprint): def get_key(self, fingerprint: str) -> KeyData | None:
return self._key_store.get(fingerprint, None) return self._key_store.get(fingerprint, None)
def set_trust(self, fingerprint, trust): def set_trust(self, fingerprint: str, trust: Trust) -> None:
self._storage.set_trust(self.jid, fingerprint, trust) self._storage.set_trust(self.jid, fingerprint, trust)
def delete_key(self, fingerprint): def delete_key(self, fingerprint: str) -> None:
self._storage.delete_key(self.jid, fingerprint) self._storage.delete_key(self.jid, fingerprint)
self._pgp.delete_key(fingerprint) self._pgp.delete_key(fingerprint)
del self._key_store[fingerprint] del self._key_store[fingerprint]
@@ -192,8 +201,8 @@ class PGPContacts:
Holds all contacts available for PGP encryption Holds all contacts available for PGP encryption
""" """
def __init__(self, pgp, storage): def __init__(self, pgp: BasePGPBackend, storage: Storage) -> None:
self._contacts = {} self._contacts: dict[JID, ContactData] = {}
self._storage = storage self._storage = storage
self._pgp = pgp self._pgp = pgp
self._load_from_storage() self._load_from_storage()
@@ -204,14 +213,12 @@ class PGPContacts:
keyring = self._pgp.get_keys() keyring = self._pgp.get_keys()
for key in keyring: for key in keyring:
log.info("Found: %s %s", key.jid, key.fingerprint) log.info("Found: %s %s", key.jid, key.fingerprint)
assert key.jid is not None
self.set_public_key(key.jid, key.fingerprint) self.set_public_key(key.jid, key.fingerprint)
def _load_from_storage(self): def _load_from_storage(self):
log.info("Load contacts from storage") log.info("Load contacts from storage")
rows = self._storage.load_contacts() rows = self._storage.load_contacts()
if rows is None:
return
for row in rows: for row in rows:
log.info("Found: %s %s", row.jid, row.fingerprint) log.info("Found: %s %s", row.jid, row.fingerprint)
try: try:
@@ -223,7 +230,9 @@ class PGPContacts:
else: else:
contact_data.add_from_db(row) contact_data.add_from_db(row)
def process_keylist(self, jid, keylist): def process_keylist(
self, jid: JID, keylist: list[PGPKeyMetadata] | None
) -> list[str]:
try: try:
contact_data = self._contacts[jid] contact_data = self._contacts[jid]
except KeyError: except KeyError:
@@ -235,7 +244,7 @@ class PGPContacts:
return missing_pub_keys return missing_pub_keys
def set_public_key(self, jid, fingerprint): def set_public_key(self, jid: JID, fingerprint: str) -> None:
try: try:
contact_data = self._contacts[jid] contact_data = self._contacts[jid]
except KeyError: except KeyError:
@@ -243,14 +252,14 @@ class PGPContacts:
else: else:
contact_data.set_public_key(fingerprint) contact_data.set_public_key(fingerprint)
def get_keys(self, jid, only_trusted=True): def get_keys(self, jid: JID, only_trusted: bool = True) -> list[KeyData]:
try: try:
contact_data = self._contacts[jid] contact_data = self._contacts[jid]
return contact_data.get_keys(only_trusted=only_trusted) return contact_data.get_keys(only_trusted=only_trusted)
except KeyError: except KeyError:
return [] return []
def get_trust(self, jid, fingerprint): def get_trust(self, jid: JID, fingerprint: str) -> Trust:
contact_data = self._contacts.get(jid, None) contact_data = self._contacts.get(jid, None)
if contact_data is None: if contact_data is None:
return Trust.UNKNOWN return Trust.UNKNOWN

View File

@@ -14,37 +14,47 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with OpenPGP Gajim Plugin. If not, see <http://www.gnu.org/licenses/>. # along with OpenPGP Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
from typing import Any
from typing import cast
import logging import logging
import sys import sys
import time import time
from collections.abc import Callable
from pathlib import Path from pathlib import Path
from nbxmpp import Node from nbxmpp import Node
from nbxmpp import StanzaMalformed from nbxmpp import StanzaMalformed
from nbxmpp.client import Client as nbxmppClient
from nbxmpp.errors import MalformedStanzaError from nbxmpp.errors import MalformedStanzaError
from nbxmpp.errors import StanzaError from nbxmpp.errors import StanzaError
from nbxmpp.exceptions import StanzaDecrypted from nbxmpp.exceptions import StanzaDecrypted
from nbxmpp.modules.openpgp import create_message_stanza from nbxmpp.modules.openpgp import create_message_stanza
from nbxmpp.modules.openpgp import create_signcrypt_node from nbxmpp.modules.openpgp import create_signcrypt_node
from nbxmpp.modules.openpgp import parse_signcrypt from nbxmpp.modules.openpgp import parse_signcrypt
from nbxmpp.modules.openpgp import PGPKeyMetadata
from nbxmpp.namespaces import Namespace from nbxmpp.namespaces import Namespace
from nbxmpp.protocol import JID
from nbxmpp.protocol import Message
from nbxmpp.structs import EncryptionData from nbxmpp.structs import EncryptionData
from nbxmpp.structs import MessageProperties from nbxmpp.structs import MessageProperties
from nbxmpp.structs import PGPKeyMetadata
from nbxmpp.structs import PGPPublicKey
from nbxmpp.structs import StanzaHandler from nbxmpp.structs import StanzaHandler
from nbxmpp.task import Task
from gajim.common import app from gajim.common import app
from gajim.common import configpaths from gajim.common import configpaths
from gajim.common.client import Client
from gajim.common.events import MessageNotSent from gajim.common.events import MessageNotSent
from gajim.common.modules.base import BaseModule from gajim.common.modules.base import BaseModule
from gajim.common.modules.util import event_node from gajim.common.modules.util import event_node
from gajim.common.structs import OutgoingMessage from gajim.common.structs import OutgoingMessage
from openpgp.backend.sql import Storage from openpgp.backend.sql import Storage
from openpgp.modules.key_store import KeyData
from openpgp.modules.key_store import PGPContacts from openpgp.modules.key_store import PGPContacts
from openpgp.modules.util import DecryptionFailed from openpgp.modules.util import DecryptionFailed
from openpgp.modules.util import ENCRYPTION_NAME from openpgp.modules.util import ENCRYPTION_NAME
from openpgp.modules.util import Key
from openpgp.modules.util import NOT_ENCRYPTED_TAGS from openpgp.modules.util import NOT_ENCRYPTED_TAGS
from openpgp.modules.util import prepare_stanza from openpgp.modules.util import prepare_stanza
from openpgp.modules.util import Trust from openpgp.modules.util import Trust
@@ -52,7 +62,7 @@ from openpgp.modules.util import Trust
if sys.platform == "win32": if sys.platform == "win32":
from openpgp.backend.pygpg import PythonGnuPG as PGPBackend from openpgp.backend.pygpg import PythonGnuPG as PGPBackend
else: else:
from openpgp.backend.gpgme import GPGME as PGPBackend from openpgp.backend.gpgme import GPGMe as PGPBackend
log = logging.getLogger("gajim.p.openpgp") log = logging.getLogger("gajim.p.openpgp")
@@ -75,7 +85,7 @@ class OpenPGP(BaseModule):
"request_secret_key", "request_secret_key",
] ]
def __init__(self, client): def __init__(self, client: Client):
BaseModule.__init__(self, client) BaseModule.__init__(self, client)
self.handlers = [ self.handlers = [
@@ -103,67 +113,88 @@ class OpenPGP(BaseModule):
log.info("Own Fingerprint at start: %s", self._fingerprint) log.info("Own Fingerprint at start: %s", self._fingerprint)
@property @property
def secret_key_available(self): def secret_key_available(self) -> bool:
return self._fingerprint is not None return self._fingerprint is not None
def get_own_key_details(self): def get_own_key_details(self) -> tuple[str | None, int | None]:
self._fingerprint, self._date = self._pgp.get_own_key_details() self._fingerprint, self._date = self._pgp.get_own_key_details()
return self._fingerprint, self._date return self._fingerprint, self._date
def generate_key(self): def generate_key(self) -> None:
self._pgp.generate_key() self._pgp.generate_key()
def set_public_key(self): def set_public_key(self) -> None:
log.info("%s => Publish public key", self._account) log.info("%s => Publish public key", self._account)
assert self._fingerprint is not None
assert self._date is not None
key = self._pgp.export_key(self._fingerprint) key = self._pgp.export_key(self._fingerprint)
assert key is not None
self._nbxmpp("OpenPGP").set_public_key(key, self._fingerprint, self._date) self._nbxmpp("OpenPGP").set_public_key(key, self._fingerprint, self._date)
def request_public_key(self, jid, fingerprint): def request_public_key(self, jid: JID, fingerprint: str) -> None:
log.info("%s => Request public key %s - %s", self._account, fingerprint, jid) log.info("%s => Request public key %s - %s", self._account, fingerprint, jid)
self._nbxmpp("OpenPGP").request_public_key( self._nbxmpp("OpenPGP").request_public_key(
jid, fingerprint, callback=self._public_key_received, user_data=fingerprint jid, fingerprint, callback=self._public_key_received, user_data=fingerprint
) )
def _public_key_received(self, task): def _public_key_received(self, task: Task) -> None:
fingerprint = task.get_user_data() fingerprint = task.get_user_data()
try: try:
result = task.finish() result = cast(PGPPublicKey | None, task.finish())
except (StanzaError, MalformedStanzaError) as error: except (StanzaError, MalformedStanzaError) as error:
log.error("%s => Public Key not found: %s", self._account, error) log.error("%s => Public Key not found: %s", self._account, error)
return return
if result is None:
log.error("%s => Public Key Node is empty", self._account)
return
imported_key = self._pgp.import_key(result.key, result.jid) imported_key = self._pgp.import_key(result.key, result.jid)
if imported_key is not None: if imported_key is not None:
self._contacts.set_public_key(result.jid, fingerprint) self._contacts.set_public_key(result.jid, fingerprint)
def set_keylist(self, keylist=None): def set_keylist(self, keylist: list[PGPKeyMetadata] | None = None) -> None:
if keylist is None: if keylist is None:
keylist = [PGPKeyMetadata(None, self._fingerprint, self._date)] assert self._fingerprint is not None
assert self._date is not None
keylist = [PGPKeyMetadata(self.own_jid, self._fingerprint, self._date)]
log.info("%s => Publish keylist", self._account) log.info("%s => Publish keylist", self._account)
self._nbxmpp("OpenPGP").set_keylist(keylist) self._nbxmpp("OpenPGP").set_keylist(keylist)
@event_node(Namespace.OPENPGP_PK) @event_node(Namespace.OPENPGP_PK)
def _keylist_notification_received(self, _con, _stanza, properties): def _keylist_notification_received(
self, _client: nbxmppClient, _stanza: Node, properties: MessageProperties
) -> None:
assert properties.pubsub_event is not None
if properties.pubsub_event.retracted: if properties.pubsub_event.retracted:
return return
keylist = properties.pubsub_event.data or [] assert properties.jid is not None
keylist: list[PGPKeyMetadata] = []
if properties.pubsub_event.data:
keylist = cast(list[PGPKeyMetadata], properties.pubsub_event.data)
self._process_keylist(keylist, properties.jid) self._process_keylist(keylist, properties.jid)
def request_keylist(self, jid=None): def request_keylist(self, jid: JID | None = None) -> None:
if jid is None: if jid is None:
jid = self.own_jid jid = self.own_jid
log.info("%s => Fetch keylist %s", self._account, jid) log.info("%s => Fetch keylist %s", self._account, jid)
self._nbxmpp("OpenPGP").request_keylist( self._nbxmpp("OpenPGP").request_keylist(
jid, callback=self._keylist_received, user_data=jid jid, callback=self._keylist_received, user_data=jid
) )
def _keylist_received(self, task): def _keylist_received(self, task: Task) -> None:
jid = task.get_user_data() jid = cast(JID, task.get_user_data())
try: try:
keylist = task.finish() keylist = cast(list[PGPKeyMetadata] | None, task.finish())
except (StanzaError, MalformedStanzaError) as error: except (StanzaError, MalformedStanzaError) as error:
log.error("%s => Keylist query failed: %s", self._account, error) log.error("%s => Keylist query failed: %s", self._account, error)
if self.own_jid.bare_match(jid) and self._fingerprint is not None: if self.own_jid.bare_match(jid) and self._fingerprint is not None:
@@ -173,7 +204,9 @@ class OpenPGP(BaseModule):
log.info("Keylist received from %s", jid) log.info("Keylist received from %s", jid)
self._process_keylist(keylist, jid) self._process_keylist(keylist, jid)
def _process_keylist(self, keylist, from_jid): def _process_keylist(
self, keylist: list[PGPKeyMetadata] | None, from_jid: JID
) -> None:
if not keylist: if not keylist:
log.warning("%s => Empty keylist received from %s", self._account, from_jid) log.warning("%s => Empty keylist received from %s", self._account, from_jid)
self._contacts.process_keylist(self.own_jid, keylist) self._contacts.process_keylist(self.own_jid, keylist)
@@ -185,14 +218,19 @@ class OpenPGP(BaseModule):
log.info("Received own keylist") log.info("Received own keylist")
for key in keylist: for key in keylist:
log.info(key.fingerprint) log.info(key.fingerprint)
for key in keylist: for key in keylist:
# Check if own fingerprint is published # Check if own fingerprint is published
if key.fingerprint == self._fingerprint: if key.fingerprint == self._fingerprint:
log.info("Own key found in keys list") log.info("Own key found in keys list")
return return
log.info("Own key not published") log.info("Own key not published")
if self._fingerprint is not None: if self._fingerprint is not None:
keylist.append(Key(self._fingerprint, self._date)) assert self._date is not None
keylist.append(
PGPKeyMetadata(self.own_jid, self._fingerprint, self._date)
)
self.set_keylist(keylist) self.set_keylist(keylist)
return return
@@ -204,10 +242,14 @@ class OpenPGP(BaseModule):
for fingerprint in missing_pub_keys: for fingerprint in missing_pub_keys:
self.request_public_key(from_jid, fingerprint) self.request_public_key(from_jid, fingerprint)
def decrypt_message(self, _con, stanza, properties: MessageProperties): def decrypt_message(
self, _client: nbxmppClient, stanza: Message, properties: MessageProperties
) -> None:
if not properties.is_openpgp: if not properties.is_openpgp:
return return
assert properties.openpgp is not None
remote_jid = properties.remote_jid remote_jid = properties.remote_jid
assert remote_jid is not None assert remote_jid is not None
@@ -249,7 +291,9 @@ class OpenPGP(BaseModule):
raise StanzaDecrypted raise StanzaDecrypted
def encrypt_message(self, message: OutgoingMessage, callback): def encrypt_message(
self, message: OutgoingMessage, callback: Callable[[OutgoingMessage], None]
) -> None:
remote_jid = message.contact.jid remote_jid = message.contact.jid
keys = self._contacts.get_keys(remote_jid) keys = self._contacts.get_keys(remote_jid)
@@ -257,12 +301,17 @@ class OpenPGP(BaseModule):
log.error("Dropping stanza to %s, because we have no key", remote_jid) log.error("Dropping stanza to %s, because we have no key", remote_jid)
return return
assert self._fingerprint is not None
keys += self._contacts.get_keys(self.own_jid) keys += self._contacts.get_keys(self.own_jid)
keys += [Key(self._fingerprint, None)] keys += [
KeyData(None, self._fingerprint, True, Trust.VERIFIED, 0) # pyright: ignore
]
payload = create_signcrypt_node( payload = create_signcrypt_node(
message.get_stanza(), [remote_jid], NOT_ENCRYPTED_TAGS message.get_stanza(), [remote_jid], NOT_ENCRYPTED_TAGS
) )
payload = str(payload).encode("utf8")
encrypted_payload, error = self._pgp.encrypt(payload, keys) encrypted_payload, error = self._pgp.encrypt(payload, keys)
if error: if error:
@@ -279,6 +328,8 @@ class OpenPGP(BaseModule):
) )
return return
assert encrypted_payload is not None
create_message_stanza( create_message_stanza(
message.get_stanza(), encrypted_payload, bool(message.get_text()) message.get_stanza(), encrypted_payload, bool(message.get_text())
) )
@@ -292,7 +343,7 @@ class OpenPGP(BaseModule):
callback(message) callback(message)
@staticmethod @staticmethod
def print_msg_to_log(stanza): def print_msg_to_log(stanza: Node) -> None:
"""Prints a stanza in a fancy way to the log""" """Prints a stanza in a fancy way to the log"""
log.debug("-" * 15) log.debug("-" * 15)
stanzastr = "\n" + stanza.__str__(fancy=True) stanzastr = "\n" + stanza.__str__(fancy=True)
@@ -300,19 +351,21 @@ class OpenPGP(BaseModule):
log.debug(stanzastr) log.debug(stanzastr)
log.debug("-" * 15) log.debug("-" * 15)
def get_keys(self, jid=None, only_trusted=True): def get_keys(
self, jid: JID | None = None, only_trusted: bool = True
) -> list[KeyData]:
if jid is None: if jid is None:
jid = self.own_jid jid = self.own_jid
return self._contacts.get_keys(jid, only_trusted=only_trusted) return self._contacts.get_keys(jid, only_trusted=only_trusted)
def clear_fingerprints(self): def clear_fingerprints(self) -> None:
self.set_keylist() self.set_keylist()
def cleanup(self): def cleanup(self) -> None:
self._storage.cleanup() self._storage.cleanup()
self._pgp = None del self._pgp
self._contacts = None del self._contacts
def get_instance(*args, **kwargs): def get_instance(*args: Any, **kwargs: Any) -> tuple[Any, str]:
return OpenPGP(*args, **kwargs), "OpenPGP" return OpenPGP(*args, **kwargs), "OpenPGP"

View File

@@ -14,9 +14,9 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with OpenPGP Gajim Plugin. If not, see <http://www.gnu.org/licenses/>. # along with OpenPGP Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
from collections import namedtuple
from enum import IntEnum from enum import IntEnum
from nbxmpp import Node
from nbxmpp.namespaces import Namespace from nbxmpp.namespaces import Namespace
ENCRYPTION_NAME = "OpenPGP" ENCRYPTION_NAME = "OpenPGP"
@@ -27,11 +27,9 @@ NOT_ENCRYPTED_TAGS = [
("no-copy", Namespace.HINTS), ("no-copy", Namespace.HINTS),
("no-permanent-store", Namespace.HINTS), ("no-permanent-store", Namespace.HINTS),
("origin-id", Namespace.SID), ("origin-id", Namespace.SID),
("thread", None), ("thread", ""),
] ]
Key = namedtuple("Key", "fingerprint date")
class Trust(IntEnum): class Trust(IntEnum):
NOT_TRUSTED = 0 NOT_TRUSTED = 0
@@ -40,19 +38,23 @@ class Trust(IntEnum):
VERIFIED = 3 VERIFIED = 3
def prepare_stanza(stanza, payload): def prepare_stanza(stanza: Node, payload: list[Node | str]) -> None:
delete_nodes(stanza, "openpgp", Namespace.OPENPGP) delete_nodes(stanza, "openpgp", Namespace.OPENPGP)
delete_nodes(stanza, "body") delete_nodes(stanza, "body")
nodes = [(node.getName(), node.getNamespace()) for node in payload] nodes: list[Node] = []
for name, namespace in nodes:
delete_nodes(stanza, name, namespace)
for node in payload: for node in payload:
if isinstance(node, str):
continue
name, namespace = node.getName(), node.getNamespace()
delete_nodes(stanza, name, namespace)
nodes.append(node)
for node in nodes:
stanza.addChild(node=node) stanza.addChild(node=node)
def delete_nodes(stanza, name, namespace=None): def delete_nodes(stanza: Node, name: str, namespace: str | None = None) -> None:
attrs = None attrs = None
if namespace is not None: if namespace is not None:
attrs = {"xmlns": Namespace.OPENPGP} attrs = {"xmlns": Namespace.OPENPGP}

View File

@@ -14,10 +14,17 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with OpenPGP Gajim Plugin. If not, see <http://www.gnu.org/licenses/>. # along with OpenPGP Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
from typing import Any
from typing import TYPE_CHECKING
import logging import logging
from collections.abc import Callable
from pathlib import Path from pathlib import Path
from gi.repository import Gdk from gi.repository import Gdk
from gi.repository import GLib
from gi.repository import Gtk from gi.repository import Gtk
from nbxmpp import JID from nbxmpp import JID
from nbxmpp.namespaces import Namespace from nbxmpp.namespaces import Namespace
@@ -25,7 +32,11 @@ from nbxmpp.namespaces import Namespace
from gajim.common import app from gajim.common import app
from gajim.common import configpaths from gajim.common import configpaths
from gajim.common import ged from gajim.common import ged
from gajim.common.client import Client
from gajim.common.const import CSSPriority from gajim.common.const import CSSPriority
from gajim.common.events import SignedIn
from gajim.common.structs import OutgoingMessage
from gajim.gtk.control import ChatControl
from gajim.gtk.dialogs import SimpleDialog from gajim.gtk.dialogs import SimpleDialog
from gajim.plugins import GajimPlugin from gajim.plugins import GajimPlugin
from gajim.plugins.plugins_i18n import _ from gajim.plugins.plugins_i18n import _
@@ -35,18 +46,21 @@ from openpgp.modules.util import ENCRYPTION_NAME
try: try:
from openpgp.modules import openpgp from openpgp.modules import openpgp
except (ImportError, OSError) as e: except (ImportError, OSError) as e:
ERROR_MSG = str(e) error_msg = str(e)
else: else:
ERROR_MSG = None error_msg = None
if TYPE_CHECKING:
from openpgp.modules.openpgp import OpenPGP
log = logging.getLogger("gajim.p.openpgp") log = logging.getLogger("gajim.p.openpgp")
class OpenPGPPlugin(GajimPlugin): class OpenPGPPlugin(GajimPlugin):
def init(self): def init(self):
if ERROR_MSG: if error_msg:
self.activatable = False self.activatable = False
self.available_text = ERROR_MSG self.available_text = error_msg
self.config_dialog = None self.config_dialog = None
return return
@@ -76,7 +90,11 @@ class OpenPGPPlugin(GajimPlugin):
self._create_paths() self._create_paths()
self._load_css() self._load_css()
def _load_css(self): @staticmethod
def get_openpgp_module(account: str) -> OpenPGP:
return app.get_client(account).get_module("OpenPGP") # pyright: ignore
def _load_css(self) -> None:
path = Path(__file__).parent / "gtk" / "style.css" path = Path(__file__).parent / "gtk" / "style.css"
try: try:
with path.open("r") as f: with path.open("r") as f:
@@ -85,59 +103,63 @@ class OpenPGPPlugin(GajimPlugin):
log.error("Error loading css: %s", exc) log.error("Error loading css: %s", exc)
return return
display = Gdk.Display.get_default()
assert display is not None
try: try:
provider = Gtk.CssProvider() provider = Gtk.CssProvider()
provider.load_from_data(bytes(css.encode("utf-8"))) provider.load_from_bytes(GLib.Bytes.new(css.encode("utf-8")))
Gtk.StyleContext.add_provider_for_screen( Gtk.StyleContext.add_provider_for_display(
Gdk.Screen.get_default(), provider, CSSPriority.DEFAULT_THEME display, provider, CSSPriority.DEFAULT_THEME
) )
except Exception: except Exception:
log.exception("Error loading application css") log.exception("Error loading application css")
@staticmethod @staticmethod
def _create_paths(): def _create_paths() -> None:
keyring_path = Path(configpaths.get("MY_DATA")) / "openpgp" keyring_path = Path(configpaths.get("MY_DATA")) / "openpgp"
if not keyring_path.exists(): if not keyring_path.exists():
keyring_path.mkdir() keyring_path.mkdir()
def signed_in(self, event): def signed_in(self, event: SignedIn) -> None:
client = app.get_client(event.account) openpgp = self.get_openpgp_module(event.account)
if client.get_module("OpenPGP").secret_key_available: if openpgp.secret_key_available:
log.info( log.info(
"%s => Publish keylist and public key after sign in", event.account "%s => Publish keylist and public key after sign in", event.account
) )
client.get_module("OpenPGP").request_keylist() openpgp.request_keylist()
client.get_module("OpenPGP").set_public_key() openpgp.set_public_key()
def activate(self): def activate(self) -> None:
for account in app.settings.get_active_accounts(): for account in app.settings.get_active_accounts():
client = app.get_client(account) client = app.get_client(account)
client.get_module("Caps").update_caps() client.get_module("Caps").update_caps()
if app.account_is_connected(account): if app.account_is_connected(account):
if client.get_module("OpenPGP").secret_key_available: openpgp = self.get_openpgp_module(account)
if openpgp.secret_key_available:
log.info( log.info(
"%s => Publish keylist and public key " "%s => Publish keylist and public key "
"after plugin activation", "after plugin activation",
account, account,
) )
client.get_module("OpenPGP").request_keylist() openpgp.request_keylist()
client.get_module("OpenPGP").set_public_key() openpgp.set_public_key()
def deactivate(self): def deactivate(self) -> None:
pass pass
@staticmethod @staticmethod
def _update_caps(_account, features): def _update_caps(_account: str, features: list[str]) -> None:
features.append("%s+notify" % Namespace.OPENPGP_PK) features.append("%s+notify" % Namespace.OPENPGP_PK)
def activate_encryption(self, chat_control): def activate_encryption(self, chat_control: ChatControl) -> bool:
account = chat_control.account account = chat_control.account
jid = chat_control.contact.jid jid = chat_control.contact.jid
client = app.get_client(account) openpgp = self.get_openpgp_module(account)
if client.get_module("OpenPGP").secret_key_available: if openpgp.secret_key_available:
keys = client.get_module("OpenPGP").get_keys(jid, only_trusted=False) keys = openpgp.get_keys(jid, only_trusted=False)
if not keys: if not keys:
client.get_module("OpenPGP").request_keylist(JID.from_string(jid)) openpgp.request_keylist(JID.from_string(jid))
return True return True
from openpgp.gtk.wizard import KeyWizard from openpgp.gtk.wizard import KeyWizard
@@ -146,12 +168,12 @@ class OpenPGPPlugin(GajimPlugin):
return False return False
@staticmethod @staticmethod
def encryption_state(_chat_control, state): def encryption_state(_chat_control: ChatControl, state: dict[str, Any]) -> None:
state["authenticated"] = True state["authenticated"] = True
state["visible"] = True state["visible"] = True
@staticmethod @staticmethod
def on_encryption_button_clicked(chat_control): def on_encryption_button_clicked(chat_control: ChatControl) -> None:
account = chat_control.account account = chat_control.account
jid = chat_control.contact.jid jid = chat_control.contact.jid
@@ -159,26 +181,31 @@ class OpenPGPPlugin(GajimPlugin):
KeyDialog(account, jid, app.window) KeyDialog(account, jid, app.window)
def _before_sendmessage(self, chat_control): def _before_sendmessage(self, chat_control: ChatControl) -> None:
account = chat_control.account account = chat_control.account
jid = chat_control.contact.jid jid = chat_control.contact.jid
client = app.get_client(account) openpgp = self.get_openpgp_module(account)
if not client.get_module("OpenPGP").secret_key_available: if not openpgp.secret_key_available:
from openpgp.gtk.wizard import KeyWizard from openpgp.gtk.wizard import KeyWizard
KeyWizard(self, account, chat_control) KeyWizard(self, account, chat_control)
return return
keys = client.get_module("OpenPGP").get_keys(jid) keys = openpgp.get_keys(jid)
if not keys: if not keys:
SimpleDialog( SimpleDialog(
_("Not Trusted"), _("There was no trusted and active key found") _("Not Trusted"), _("There was no trusted and active key found")
) )
chat_control.sendmessage = False chat_control.sendmessage = False
@staticmethod def _encrypt_message(
def _encrypt_message(client, obj, callback): self,
if not client.get_module("OpenPGP").secret_key_available: client: Client,
message: OutgoingMessage,
callback: Callable[[OutgoingMessage], None],
) -> None:
openpgp = self.get_openpgp_module(client.account)
if not openpgp.secret_key_available:
return return
client.get_module("OpenPGP").encrypt_message(obj, callback) openpgp.encrypt_message(message, callback)

View File

@@ -20,7 +20,6 @@ exclude = [
"**/__pycache__", "**/__pycache__",
".git", ".git",
".venv", ".venv",
"openpgp/*",
] ]
[tool.ruff] [tool.ruff]

File diff suppressed because it is too large Load Diff

1
typings/gpg/__init__.py Normal file
View File

@@ -0,0 +1 @@
from .core import Context # noqa: F401

67
typings/gpg/core.pyi Normal file
View File

@@ -0,0 +1,67 @@
from typing import Any
from collections.abc import Iterator
from gpg.results import EncryptResult
from gpg.results import SignResult
class GpgmeWrapper(object): ...
class Context(GpgmeWrapper):
def __init__(
self,
armor: bool = ...,
textmode: bool = ...,
offline: bool = ...,
signers: list[str] = [],
pinentry_mode: str = ...,
protocol: str = ...,
wrapped: Any | None = ...,
home_dir: str | None = ...,
) -> None: ...
def __enter__(self) -> Context: ...
def __exit__(self, type: Any, value: Any, tb: Any) -> bool: ...
def encrypt(
self,
plaintext: bytes,
recipients: list[str] = [],
sign: bool = ...,
sink: Any | None = ...,
passphrase: str | None = ...,
always_trust: bool = ...,
add_encrypt_to: bool = ...,
prepare: bool = ...,
expect_sign: bool = ...,
compress: bool = ...,
) -> tuple[bytes, EncryptResult, SignResult]: ...
def decrypt(
self,
ciphertext: bytes,
sink: Any | None = ...,
passphrase: str | None = ...,
verify: bool = ...,
filter_signatures: bool = ...,
) -> tuple[bytes, str, str]: ...
def key_import(self, data: bytes) -> str: ...
def key_export_minimal(self, pattern: Any | None = ...) -> bytes | None: ...
def keylist(
self,
pattern: Any | None = ...,
secret: bool = ...,
mode: str = ...,
source: Any | None = None,
) -> Iterator[Any]: ...
def create_key(
self,
userid: str,
algorithm: str | None = ...,
expires_in: int = ...,
expires: bool = ...,
sign: bool = ...,
encrypt: bool = ...,
certify: bool = ...,
authenticate: bool = ...,
passphrase: str | None = ...,
force: bool = ...,
) -> str: ...
def get_key(self, fpr: str, secret: bool = ...) -> Any | None: ...

36
typings/gpg/errors.pyi Normal file
View File

@@ -0,0 +1,36 @@
from typing import Any
class GpgError(Exception):
error: Any | None
context: str | None
result: Any | None
@property
def code(self) -> int: ...
@property
def code_str(self) -> str: ...
@property
def source(self) -> int: ...
@property
def source_str(self) -> str: ...
class GPGMEError(GpgError):
@property
def message(self) -> str: ...
def getstring(self) -> str: ...
def getcode(self) -> int: ...
def getsource(self) -> int: ...
class KeyNotFound(GPGMEError, KeyError):
keystr: str
class EncryptionError(GpgError): ...
class InvalidRecipients(EncryptionError): ...
class DecryptionError(GpgError): ...
class UnsupportedAlgorithm(DecryptionError): ...
class SigningError(GpgError): ...
class InvalidSigners(SigningError): ...
class VerificationError(GpgError): ...
class BadSignatures(VerificationError): ...
class MissingSignatures(VerificationError): ...

41
typings/gpg/results.pyi Normal file
View File

@@ -0,0 +1,41 @@
from typing import Any
class Result(object): ...
class InvalidKey(Result): ...
class EncryptResult(Result):
invalid_recipients: list[Any]
class Recipient(Result): ...
class DecryptResult(Result):
recipients: Recipient
class NewSignature(Result): ...
class SignResult(Result):
invalid_signers: InvalidKey
signatures: NewSignature
class Notation(Result): ...
class Signature(Result):
_type = dict(wrong_key_usage=bool, chain_model=bool, is_de_vs=bool)
notations: Notation
class VerifyResult(Result):
signatures: Signature
class ImportStatus(Result): ...
class ImportResult(Result):
imports: ImportStatus
class GenkeyResult(Result):
_type = dict(primary=bool, sub=bool)
class KeylistResult(Result):
_type = dict(truncated=bool)
class VFSMountResult(Result): ...
class EngineInfo(Result): ...