[openpgp] Port to Gtk4
This commit is contained in:
97
openpgp/backend/base.py
Normal file
97
openpgp/backend/base.py
Normal 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
|
||||||
@@ -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)
|
||||||
|
|||||||
86
openpgp/backend/gpgme_types.py
Normal file
86
openpgp/backend/gpgme_types.py
Normal 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]
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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:]
|
||||||
|
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
1
typings/gpg/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .core import Context # noqa: F401
|
||||||
67
typings/gpg/core.pyi
Normal file
67
typings/gpg/core.pyi
Normal 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
36
typings/gpg/errors.pyi
Normal 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
41
typings/gpg/results.pyi
Normal 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): ...
|
||||||
Reference in New Issue
Block a user