libervia-backend/libervia/backend/plugins/plugin_xep_0373/models.py
2026-03-31 12:57:17 +02:00

273 lines
9.9 KiB
Python

#!/usr/bin/env python3
# Libervia plugin for OpenPGP for XMPP
# Copyright (C) 2025-2026 Jérôme Poisosn (goffi@goffi.org)
# Copyright (C) 2022-2026 Tim Henkes (me@syndace.dev)
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program 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 Affero General Public License for more details.
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from abc import ABC, abstractmethod
class GPGPublicKeyBase(ABC):
"""
Interface describing a GPG public key.
"""
@property
@abstractmethod
def fingerprint(self) -> str:
"""
@return: The OpenPGP v4 fingerprint string of this public key.
"""
class GPGSecretKeyBase(ABC):
"""
Interface descibing a GPG secret key.
"""
@property
@abstractmethod
def public_key(self) -> GPGPublicKeyBase:
"""
@return: The public key corresponding to this secret key.
"""
class GPGProviderBase(ABC):
"""
Interface describing a GPG provider, i.e. a library or framework providing GPG
encryption, signing and key management.
All methods may raise :class:`GPGProviderError` in addition to those exception types
listed explicitly.
# TODO: Check keys for revoked, disabled and expired everywhere and exclude those (?)
"""
@abstractmethod
def export_public_key(self, public_key: GPGPublicKeyBase) -> bytes:
"""Export a public key in a key material packet according to RFC 4880 §5.5.
Do not use OpenPGP's ASCII Armor.
@param public_key: The public key to export.
@return: The packet containing the exported public key.
@raise UnknownKey: if the public key is not available.
"""
@abstractmethod
def import_public_key(self, packet: bytes) -> GPGPublicKeyBase:
"""import a public key from a key material packet according to RFC 4880 §5.5.
OpenPGP's ASCII Armor is not used.
@param packet: A packet containing an exported public key.
@return: The public key imported from the packet.
@raise InvalidPacket: if the packet is either syntactically or semantically deemed
invalid.
@warning: Only packets of version 4 or higher may be accepted, packets below
version 4 MUST be rejected.
"""
@abstractmethod
def backup_secret_key(self, secret_key: GPGSecretKeyBase) -> bytes:
"""Export a secret key for transfer according to RFC 4880 §11.1.
Do not encrypt the secret data, i.e. set the octet indicating string-to-key usage
conventions to zero in the corresponding secret-key packet according to RFC 4880
§5.5.3. Do not use OpenPGP's ASCII Armor.
@param secret_key: The secret key to export.
@return: The binary blob containing the exported secret key.
@raise UnknownKey: if the secret key is not available.
"""
@abstractmethod
def restore_secret_keys(self, data: bytes) -> set[GPGSecretKeyBase]:
"""Restore secret keys exported for transfer according to RFC 4880 §11.1.
The secret data is not encrypted, i.e. the octet indicating string-to-key usage
conventions in the corresponding secret-key packets according to RFC 4880 §5.5.3
are set to zero. OpenPGP's ASCII Armor is not used.
@param data: Concatenation of one or more secret keys exported for transfer.
@return: The secret keys imported from the data.
@raise InvalidPacket: if the data or one of the packets included in the data is
either syntactically or semantically deemed invalid.
@warning: Only packets of version 4 or higher may be accepted, packets below
version 4 MUST be rejected.
"""
@abstractmethod
def encrypt_symmetrically(self, plaintext: bytes, password: str) -> bytes:
"""Encrypt data symmetrically according to RFC 4880 §5.3.
The password is used to build a Symmetric-Key Encrypted Session Key packet which
precedes the Symmetrically Encrypted Data packet that holds the encrypted data.
@param plaintext: The data to encrypt.
@param password: The password to encrypt the data with.
@return: The encrypted data.
"""
@abstractmethod
def decrypt_symmetrically(self, ciphertext: bytes, password: str) -> bytes:
"""Decrypt data symmetrically according to RFC 4880 §5.3.
The ciphertext consists of a Symmetrically Encrypted Data packet that holds the
encrypted data, preceded by a Symmetric-Key Encrypted Session Key packet using the
password.
@param ciphertext: The ciphertext.
@param password: The password to decrypt the data with.
@return: The plaintext.
@raise DecryptionFailed: on decryption failure.
"""
@abstractmethod
def sign(self, data: bytes, secret_keys: set[GPGSecretKeyBase]) -> bytes:
"""Sign some data.
OpenPGP's ASCII Armor is not used.
@param data: The data to sign.
@param secret_keys: The secret keys to sign the data with.
@return: The OpenPGP message carrying the signed data.
"""
@abstractmethod
def sign_detached(self, data: bytes, secret_keys: set[GPGSecretKeyBase]) -> bytes:
"""Sign some data. Create the signature detached from the data.
OpenPGP's ASCII Armor is not used.
@param data: The data to sign.
@param secret_keys: The secret keys to sign the data with.
@return: The OpenPGP message carrying the detached signature.
"""
@abstractmethod
def verify(self, signed_data: bytes, public_keys: set[GPGPublicKeyBase]) -> bytes:
"""Verify signed data.
OpenPGP's ASCII Armor is not used.
@param signed_data: The signed data as an OpenPGP message.
@param public_keys: The public keys to verify the signature with.
@return: The verified and unpacked data.
@raise VerificationFailed: if the data could not be verified.
@warning: For implementors: it has to be confirmed that a valid signature by one
of the public keys is available.
"""
@abstractmethod
def verify_detached(
self, data: bytes, signature: bytes, public_keys: set[GPGPublicKeyBase]
) -> None:
"""Verify signed data, where the signature was created detached from the data.
OpenPGP's ASCII Armor is not used.
@param data: The data.
@param signature: The signature as an OpenPGP message.
@param public_keys: The public keys to verify the signature with.
@raise VerificationFailed: if the data could not be verified.
@warning: For implementors: it has to be confirmed that a valid signature by one
of the public keys is available.
"""
@abstractmethod
def encrypt(
self,
plaintext: bytes,
public_keys: set[GPGPublicKeyBase],
signing_keys: set[GPGSecretKeyBase] | None = None,
) -> bytes:
"""Encrypt and optionally sign some data.
OpenPGP's ASCII Armor is not used.
@param plaintext: The data to encrypt and optionally sign.
@param public_keys: The public keys to encrypt the data for.
@param signing_keys: The secret keys to sign the data with.
@return: The OpenPGP message carrying the encrypted and optionally signed data.
"""
@abstractmethod
def decrypt(
self,
ciphertext: bytes,
secret_keys: set[GPGSecretKeyBase],
public_keys: set[GPGPublicKeyBase] | None = None,
) -> bytes:
"""Decrypt and optionally verify some data.
OpenPGP's ASCII Armor is not used.
@param ciphertext: The encrypted and optionally signed data as an OpenPGP message.
@param secret_keys: The secret keys to attempt decryption with.
@param public_keys: The public keys to verify the optional signature with.
@return: The decrypted, optionally verified and unpacked data.
@raise DecryptionFailed: on decryption failure.
@raise VerificationFailed: if the data could not be verified.
@warning: For implementors: it has to be confirmed that the data was decrypted
using one of the secret keys and that a valid signature by one of the public
keys is available in case the data is signed.
"""
@abstractmethod
def list_public_keys(self, user_id: str) -> set[GPGPublicKeyBase]:
"""List public keys.
@param user_id: The user id.
@return: The set of public keys available for this user id.
"""
@abstractmethod
def list_secret_keys(self, user_id: str) -> set[GPGSecretKeyBase]:
"""List secret keys.
@param user_id: The user id.
@return: The set of secret keys available for this user id.
"""
@abstractmethod
def can_sign(self, public_key: GPGPublicKeyBase) -> bool:
"""
@return: Whether the public key belongs to a key pair capable of signing.
"""
@abstractmethod
def can_encrypt(self, public_key: GPGPublicKeyBase) -> bool:
"""
@return: Whether the public key belongs to a key pair capable of encryption.
"""
@abstractmethod
def create_key(self, user_id: str) -> GPGSecretKeyBase:
"""Create a new GPG key, capable of signing and encryption.
The key is generated without password protection and without expiration. If a key
with the same user id already exists, a new key is created anyway.
@param user_id: The user id to assign to the new key.
@return: The new key.
"""