// SPDX-FileCopyrightText: 2023 Greenbone AG
//
// SPDX-License-Identifier: GPL-2.0-or-later WITH x11vnc-openssl-exception

//! Verifies a feed
//!
//! It includes a HashVerifier that loads the hashsum file and verify for each entry that the given
//! hashsum is equal to the calculated hashsum.
//! This is required to prevent load modified nasl scripts.
//! If you want to manipulate the feed you have to create a new hashsum file otherwise the modificated data will not
//! be loaded

use std::{
    fs::File,
    io::{self, BufRead, Read},
    path::{Path, PathBuf},
};

use crate::nasl::syntax::{LoadError, Loader};
use hex::encode;
use sha2::{Digest, Sha256};

use openpgp::{
    Cert, KeyHandle,
    parse::Parse,
    parse::stream::{
        DetachedVerifierBuilder, GoodChecksum, MessageLayer, MessageStructure, VerificationHelper,
    },
    policy::StandardPolicy,
};
use sequoia_ipc::keybox::{Keybox, KeyboxRecord};
use sequoia_openpgp as openpgp;
use thiserror::Error;

#[derive(Debug, Clone, PartialEq, Error)]
/// Defines error cases that can happen while verifying
pub enum Error {
    #[error("Incorrect feed.")]
    /// Corrupt sums file
    SumsFileCorrupt(Hasher),
    #[error("Unable to load file: {0}")]
    /// Unable to load the file
    LoadError(#[from] LoadError),
    #[error("Invalid hash for file with key '{key}'. Expected '{expected}', found '{actual}'.")]
    /// Invalid hash.
    HashInvalid {
        /// The hash within the sums file
        expected: String,
        /// The calculated hash
        actual: String,
        /// The key of the file
        key: String,
    },
    #[error("Bad signature: {0}")]
    /// Bad Signature
    BadSignature(String),
    #[error(
        "Signature check is enabled but there is no keyring. Set the GNUPGHOME environment variable"
    )]
    /// Missing keyring
    MissingKeyring,
}

struct VHelper {
    keyring: String,
    not_before: Option<std::time::SystemTime>,
    not_after: std::time::SystemTime,
}

impl VHelper {
    fn new(keyring: String) -> Self {
        Self {
            keyring,
            not_before: None,
            not_after: std::time::SystemTime::now(),
        }
    }
}

impl VerificationHelper for VHelper {
    fn get_certs(&mut self, _ids: &[KeyHandle]) -> openpgp::Result<Vec<Cert>> {
        let file = File::open(self.keyring.as_str())?;
        let kbx = Keybox::from_reader(file)?;

        let certs = kbx
            // Keep only records which were parsed successfully.
            .filter_map(|kbx_record| kbx_record.ok())
            // Map the OpenPGP records to the contained certs.
            .filter_map(|kbx_record| match kbx_record {
                KeyboxRecord::OpenPGP(r) => Some(r.cert()),
                _ => None,
            })
            .collect::<openpgp::Result<Vec<Cert>>>()?;
        Ok(certs)
    }

    fn check(&mut self, structure: MessageStructure) -> openpgp::Result<()> {
        for layer in structure.into_iter() {
            match layer {
                MessageLayer::SignatureGroup { results } => {
                    for result in results {
                        match result {
                            Ok(GoodChecksum { sig, ka, .. }) => {
                                match (
                                    sig.signature_creation_time(),
                                    self.not_before,
                                    self.not_after,
                                ) {
                                    (None, _, _) => {
                                        eprintln!("Malformed signature:");
                                    }
                                    (Some(t), Some(not_before), not_after) => {
                                        if t < not_before {
                                            eprintln!(
                                                "Signature by {:X} was created before \
                                                 the --not-before date.",
                                                ka.key().fingerprint()
                                            );
                                        } else if t > not_after {
                                            eprintln!(
                                                "Signature by {:X} was created after \
                                                 the --not-after date.",
                                                ka.key().fingerprint()
                                            );
                                        }
                                    }
                                    (Some(t), None, not_after) => {
                                        if t > not_after {
                                            eprintln!(
                                                "Signature by {:X} was created after \
                                                 the --not-after date.",
                                                ka.key().fingerprint()
                                            );
                                        }
                                    }
                                };
                            }
                            Err(e) => return Err(anyhow::Error::msg(e.to_string())),
                        }
                    }
                }
                MessageLayer::Compression { .. } => (),
                _ => unreachable!(),
            }
        }
        Ok(())
    }
}

/// For signature check the GNUPGHOME environment variable
/// must be set with the path to the keyring.
/// If this is satisfied, the signature check is performed
pub fn check_signature<P>(path: &P) -> Result<(), Error>
where
    P: AsRef<Path> + ?Sized,
{
    let mut gnupghome = match std::env::var("GNUPGHOME") {
        Ok(v) => v,
        Err(_) => {
            return Err(Error::MissingKeyring);
        }
    };
    gnupghome.push_str("/pubring.kbx");

    let helper = VHelper::new(gnupghome);

    let sign_path = path.as_ref().to_path_buf().join("sha256sums.asc");
    let mut sig_file = File::open(&sign_path)
        .unwrap_or_else(|e| panic!("Could not find signature at {sign_path:?}. {e}"));
    let mut signature = Vec::new();
    let _ = sig_file.read_to_end(&mut signature);

    let data_path = path.as_ref().to_path_buf().join("sha256sums");
    let mut data_file = File::open(data_path).unwrap();
    let mut data = Vec::new();
    let _ = data_file.read_to_end(&mut data);

    let v = match DetachedVerifierBuilder::from_bytes(&signature[..]) {
        Ok(v) => v,
        Err(_) => {
            return Err(Error::BadSignature(
                "Signature verification failed".to_string(),
            ));
        }
    };

    let p = &StandardPolicy::new();
    if let Ok(mut verifier) = v.with_policy(p, None, helper) {
        match verifier.verify_bytes(data) {
            Ok(_) => return Ok(()),
            Err(e) => return Err(Error::BadSignature(e.to_string())),
        }
    };
    Err(Error::BadSignature(
        "Signature verification failed".to_string(),
    ))
}

/// Trait for signature check
pub trait SignatureChecker {
    /// For signature check the GNUPGHOME environment variable
    /// must be set with the path to the keyring.
    /// If this is satisfied, the signature check is performed
    fn signature_check(feed_path: &str) -> Result<(), Error> {
        check_signature(feed_path)
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
/// Hasher implements the used hashing algorithm to calculate the hashsum
pub enum Hasher {
    /// Sha256
    Sha256,
}

/// Computes hash of a given reader
fn compute_hash_with<H>(
    reader: &mut dyn BufRead,
    hasher: &dyn Fn() -> H,
    key: &str,
) -> Result<String, Error>
where
    H: Digest,
{
    let mut buffer = [0; 1024];
    let mut hasher = hasher();
    let ioma = |e| LoadError::from((key, e));

    loop {
        let count = reader.read(&mut buffer).map_err(ioma)?;
        if count == 0 {
            break;
        }
        hasher.update(&buffer[..count]);
    }
    let result = hasher.finalize();
    let result = encode(&result[..]);
    Ok(result)
}

impl Hasher {
    /// Returns the name of the used sums file
    pub fn sum_file(&self) -> &str {
        match self {
            Hasher::Sha256 => "sha256sums",
        }
    }

    /// Returns the hash of a given reader and key
    fn hash(&self, reader: &mut dyn BufRead, key: &str) -> Result<String, Error> {
        let hasher = match self {
            Hasher::Sha256 => &Sha256::new,
        };
        compute_hash_with(reader, hasher, key)
    }
}

/// Loads a given hashsums file and lazily verifies the loaded filename key of the sums file and verifies
/// the hash within the sums file with an calculated hash of the found content.
pub struct HashSumNameLoader<'a> {
    reader: &'a Loader,
    hasher: Hasher,
    buf: io::Lines<Box<dyn BufRead>>,
}

/// Loads hashsum verified names of the feed based on a sum file.
impl<'a> HashSumNameLoader<'a> {
    fn new(buf: io::Lines<Box<dyn BufRead>>, reader: &'a Loader, hasher: Hasher) -> Self {
        Self {
            reader,
            hasher,
            buf,
        }
    }

    /// Returns a sha256 implementation of HashSumNameLoader
    pub fn sha256(reader: &'a Loader) -> Result<HashSumNameLoader<'a>, Error> {
        let buf = reader
            .as_bufreader(Hasher::Sha256.sum_file())
            .map(|x| x.lines())
            .map_err(Error::LoadError)?;
        Ok(Self::new(buf, reader, Hasher::Sha256))
    }

    /// Returns the hashsum of the sums file
    pub fn sumfile_hash(&self) -> Result<String, Error> {
        self.hasher.hash(
            &mut self.reader.as_bufreader(self.hasher.sum_file())?,
            self.hasher.sum_file(),
        )
    }
}

impl<'a> Iterator for HashSumNameLoader<'a> {
    type Item = Result<HashSumFileItem<'a>, Error>;

    fn next(&mut self) -> Option<Self::Item> {
        match self.buf.next()? {
            Ok(line) => {
                let (hashsum, file_name) = match line.rsplit_once("  ") {
                    Some((hashsum, file_name)) => (hashsum, file_name),
                    None => return Some(Err(Error::SumsFileCorrupt(self.hasher.clone()))),
                };

                Some(Ok(HashSumFileItem {
                    file_name: file_name.to_string(),
                    hashsum: hashsum.to_string(),
                    hasher: Some(self.hasher.clone()),
                    reader: self.reader,
                }))
            }
            Err(_) => Some(Err(Error::SumsFileCorrupt(self.hasher.clone()))),
        }
    }
}

/// Contains all information  necessary to do a hash sum check
pub struct HashSumFileItem<'a> {
    pub file_name: String,
    pub hashsum: String,
    pub hasher: Option<Hasher>,
    pub reader: &'a Loader,
}

impl HashSumFileItem<'_> {
    /// Verifies Hashsum
    pub fn verify(&self) -> Result<(), Error> {
        if let Some(hasher) = &self.hasher {
            let hashsum = hasher.hash(
                &mut self.reader.as_bufreader(&self.file_name)?,
                &self.file_name,
            )?;
            if self.hashsum != hashsum {
                return Err(Error::HashInvalid {
                    expected: self.hashsum.clone(),
                    actual: hashsum,
                    key: self.file_name.clone(),
                });
            }
        }
        Ok(())
    }

    /// returns file name
    pub fn get_filename(&self) -> String {
        self.file_name.clone()
    }

    /// returns hash sum
    pub fn get_hashsum(&self) -> String {
        self.hashsum.clone()
    }
}

fn get_all_plugins(loader: &Loader) -> Vec<PathBuf> {
    let mut files = Vec::new();
    let rp = loader.root_path();
    for e in walkdir::WalkDir::new(rp).into_iter().filter_map(|e| e.ok()) {
        if e.path().extension().is_some_and(|ext| ext == "nasl") {
            let relative_path = e.path().strip_prefix(Path::new(&rp)).unwrap();
            files.push(relative_path.to_owned());
        }
    }
    files
}

pub struct FakeVerifier<'a> {
    loader: &'a Loader,
    files: Vec<PathBuf>,
}

impl<'a> FakeVerifier<'a> {
    pub fn new(loader: &'a Loader) -> Self {
        Self {
            loader,
            files: get_all_plugins(loader),
        }
    }
}

impl<'a> Iterator for FakeVerifier<'a> {
    type Item = Result<HashSumFileItem<'a>, Error>;

    fn next(&mut self) -> Option<Self::Item> {
        self.files.pop().map(|file| {
            // Compute the hash sum in advance so that the
            // check will always succeed.
            let file_name = file.as_path().to_str().unwrap().to_owned();
            Ok(HashSumFileItem {
                file_name,
                hashsum: String::new(),
                hasher: None,
                reader: self.loader,
            })
        })
    }
}
