#!/usr/bin/env python3 # Libervia plugin for OpenPGP for XMPP # Copyright (C) 2025-2026 Jérôme Poisosn (goffi@goffi.org) # Copyright (C) 2022-2024 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 . 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. """