273 lines
9.9 KiB
Python
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.
|
|
"""
|