//! Implement credentials for storing encrypted client keys.

use serde::{Deserialize, Serialize};

use crate::{
    cipher::Key,
    sop::{OpenPgpCert, OpenPgpKey, Sop, SopCard, SopError},
};

/// A credential chunk.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Credential {
    kind: CredentialKind,
    encrypted_client_key: Vec<u8>,
}

impl Credential {
    /// Create a new credential using the specified credential encryption method.
    pub fn new(method: &CredentialMethod, key: &Key) -> Result<Self, CredentialError> {
        Ok(Self {
            kind: method.kind(),
            encrypted_client_key: method.encrypt(key)?,
        })
    }

    /// Try to decrypt using the provided method.
    pub fn decrypt(&self, method: &CredentialMethod) -> Result<Key, CredentialError> {
        method.decrypt(&self.encrypted_client_key)
    }
}

/// Kind of credential. Determines how the client key is encrypted.
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum CredentialKind {
    /// An OpenPGP software key.
    #[serde(rename = "openpgp-soft")]
    OpenPgpSoft,

    /// An OpenPGP key on an OpenPGP card.
    #[serde(rename = "openpgp-card")]
    OpenPgpCard,
}

/// Encryption and decryption of client keys for a credential.
pub enum CredentialMethod {
    /// A software OpenPGP key.
    OpenPgpSoft {
        /// SOP implementation to use
        sop: Sop,
        /// Private OpenPGP key to use.
        key: OpenPgpKey,
    },
    /// A key on an OpenPGP card.
    OpenPgpCard {
        /// SOP-like program that supports OpenPGP cards.
        sop_card: SopCard,
        /// OpenPGP certificate to use to pick correct OpenPGP card.
        cert: OpenPgpCert,
    },
}

impl CredentialMethod {
    /// Create an OpenPGP software key method.
    pub fn openpgp_soft(sop: Sop, key: OpenPgpKey) -> Self {
        Self::OpenPgpSoft { sop, key }
    }

    /// Create an OpenPGP card method.
    pub fn openpgp_card(sop_card: SopCard, cert: OpenPgpCert) -> Self {
        Self::OpenPgpCard { sop_card, cert }
    }

    /// Return kind of credential method.
    pub fn kind(&self) -> CredentialKind {
        match self {
            Self::OpenPgpSoft { .. } => CredentialKind::OpenPgpSoft,
            Self::OpenPgpCard { .. } => CredentialKind::OpenPgpCard,
        }
    }

    /// Encrypt a client key.
    pub fn encrypt(&self, client_key: &Key) -> Result<Vec<u8>, CredentialError> {
        match self {
            Self::OpenPgpSoft { sop, key } => {
                let cert = sop
                    .extract_cert(key)
                    .map_err(CredentialError::SopExtractCert)?;
                sop.encrypt(&[cert], client_key.as_slice())
                    .map_err(CredentialError::SopEncrypt)
            }
            Self::OpenPgpCard { sop_card, cert } => sop_card
                .encrypt(std::slice::from_ref(cert), client_key.as_slice())
                .map_err(CredentialError::SopEncrypt),
        }
    }

    /// Decrypt a client key.
    pub fn decrypt(&self, ciphertext: &[u8]) -> Result<Key, CredentialError> {
        match self {
            Self::OpenPgpSoft { sop, key } => {
                let key_material = sop
                    .decrypt(key, ciphertext)
                    .map_err(CredentialError::SopDecrypt)?;
                Ok(Key::from(key_material))
            }
            Self::OpenPgpCard { sop_card, cert } => {
                let key_material = sop_card
                    .decrypt(cert, ciphertext)
                    .map_err(CredentialError::SopDecrypt)?;
                Ok(Key::from(key_material))
            }
        }
    }
}

/// Errors from credential module.
#[derive(Debug, thiserror::Error)]
pub enum CredentialError {
    /// Can't extract cert from key with SOP.
    #[error("failed to extract certificate from OpenPGP key")]
    SopExtractCert(#[source] SopError),

    /// Can't encrypt with SOP.
    #[error("failed to encrypt client key with SOP")]
    SopEncrypt(#[source] SopError),

    /// Can't decrypt with SOP
    #[error("failed to decrypt client key with SOP")]
    SopDecrypt(#[source] SopError),
}

#[cfg(test)]
mod test {
    use super::*;

    use crate::{cipher::Key, sop::Sop};

    fn sop() -> Sop {
        Sop::default()
    }

    fn openpgp_key() -> OpenPgpKey {
        sop().generate_key("Alice <alice@example.com>").unwrap()
    }

    fn opengpg_soft_method() -> CredentialMethod {
        CredentialMethod::openpgp_soft(sop(), openpgp_key())
    }

    #[test]
    fn roundtrip_sop_method() {
        let method = opengpg_soft_method();
        let key = Key::default();
        let encrypted = method.encrypt(&key).unwrap();
        let decrypted = method.decrypt(&encrypted).unwrap();
        assert_eq!(key, decrypted);
    }

    #[test]
    fn roundtrip_sop_credential() {
        let method = opengpg_soft_method();
        let key = Key::default();
        let cred = Credential::new(&method, &key).unwrap();

        let ser = serde_json::to_string(&cred).unwrap();
        let de: Credential = serde_json::from_str(&ser).unwrap();
        let decrypted_key = de.decrypt(&method).unwrap();

        assert_eq!(key, decrypted_key);
    }
}
