#!/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.
        """
