//! Bindings to winapi's certificate-store related APIs.

use std::cmp;
use std::ffi::OsStr;
use std::fmt;
use std::io;
use std::mem;
use std::os::windows::prelude::*;
use std::ptr;

use windows_sys::Win32::Security::Cryptography;

use crate::cert_context::CertContext;
use crate::ctl_context::CtlContext;
use crate::Inner;

/// Representation of certificate store on Windows, wrapping a `HCERTSTORE`.
pub struct CertStore(Cryptography::HCERTSTORE);

unsafe impl Sync for CertStore {}
unsafe impl Send for CertStore {}

impl fmt::Debug for CertStore {
    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
        fmt.debug_struct("CertStore").finish()
    }
}

impl Drop for CertStore {
    fn drop(&mut self) {
        unsafe {
            Cryptography::CertCloseStore(self.0, 0);
        }
    }
}

impl Clone for CertStore {
    fn clone(&self) -> CertStore {
        unsafe { CertStore(Cryptography::CertDuplicateStore(self.0)) }
    }
}

inner!(CertStore, Cryptography::HCERTSTORE);

/// Argument to the `add_cert` function indicating how a certificate should be
/// added to a `CertStore`.
pub enum CertAdd {
    /// The function makes no check for an existing matching certificate or link
    /// to a matching certificate. A new certificate is always added to the
    /// store. This can lead to duplicates in a store.
    Always = Cryptography::CERT_STORE_ADD_ALWAYS as isize,

    /// If a matching certificate or a link to a matching certificate exists,
    /// the operation fails.
    New = Cryptography::CERT_STORE_ADD_NEW as isize,

    /// If a matching certificate or a link to a matching certificate exists and
    /// the NotBefore time of the existing context is equal to or greater than
    /// the NotBefore time of the new context being added, the operation fails.
    ///
    /// If the NotBefore time of the existing context is less than the NotBefore
    /// time of the new context being added, the existing certificate or link is
    /// deleted and a new certificate is created and added to the store. If a
    /// matching certificate or a link to a matching certificate does not exist,
    /// a new link is added.
    Newer = Cryptography::CERT_STORE_ADD_NEWER as isize,

    /// If a matching certificate or a link to a matching certificate exists and
    /// the NotBefore time of the existing context is equal to or greater than
    /// the NotBefore time of the new context being added, the operation fails.
    ///
    /// If the NotBefore time of the existing context is less than the NotBefore
    /// time of the new context being added, the existing context is deleted
    /// before creating and adding the new context. The new added context
    /// inherits properties from the existing certificate.
    NewerInheritProperties = Cryptography::CERT_STORE_ADD_NEWER_INHERIT_PROPERTIES as isize,

    /// If a link to a matching certificate exists, that existing certificate or
    /// link is deleted and a new certificate is created and added to the store.
    /// If a matching certificate or a link to a matching certificate does not
    /// exist, a new link is added.
    ReplaceExisting = Cryptography::CERT_STORE_ADD_REPLACE_EXISTING as isize,

    /// If a matching certificate exists in the store, the existing context is
    /// not replaced. The existing context inherits properties from the new
    /// certificate.
    ReplaceExistingInheritProperties =
        Cryptography::CERT_STORE_ADD_REPLACE_EXISTING_INHERIT_PROPERTIES as isize,

    /// If a matching certificate or a link to a matching certificate exists,
    /// that existing certificate or link is used and properties from the
    /// new certificate are added. The function does not fail, but it does
    /// not add a new context. The existing context is duplicated and returned.
    ///
    /// If a matching certificate or a link to a matching certificate does
    /// not exist, a new certificate is added.
    UseExisting = Cryptography::CERT_STORE_ADD_USE_EXISTING as isize,
}

impl CertStore {
    /// Opens up the specified key store within the context of the current user.
    ///
    /// Common valid values for `which` are "My", "Root", "Trust", "CA".
    /// Additonal MSDN docs https://docs.microsoft.com/en-us/windows/win32/api/wincrypt/nf-wincrypt-certopenstore#remarks
    pub fn open_current_user(which: &str) -> io::Result<CertStore> {
        unsafe {
            let data = OsStr::new(which)
                .encode_wide()
                .chain(Some(0))
                .collect::<Vec<_>>();
            let store = Cryptography::CertOpenStore(
                Cryptography::CERT_STORE_PROV_SYSTEM_W,
                Cryptography::CERT_QUERY_ENCODING_TYPE::default(),
                Cryptography::HCRYPTPROV_LEGACY::default(),
                Cryptography::CERT_SYSTEM_STORE_CURRENT_USER_ID
                    << Cryptography::CERT_SYSTEM_STORE_LOCATION_SHIFT,
                data.as_ptr() as *mut _,
            );
            if !store.is_null() {
                Ok(CertStore(store))
            } else {
                Err(io::Error::last_os_error())
            }
        }
    }

    /// Opens up the specified key store within the context of the local machine.
    ///
    /// Common valid values for `which` are "My", "Root", "Trust", "CA".
    /// Additonal MSDN docs https://docs.microsoft.com/en-us/windows/win32/api/wincrypt/nf-wincrypt-certopenstore#remarks
    pub fn open_local_machine(which: &str) -> io::Result<CertStore> {
        unsafe {
            let data = OsStr::new(which)
                .encode_wide()
                .chain(Some(0))
                .collect::<Vec<_>>();
            let store = Cryptography::CertOpenStore(
                Cryptography::CERT_STORE_PROV_SYSTEM_W,
                Cryptography::CERT_QUERY_ENCODING_TYPE::default(),
                Cryptography::HCRYPTPROV_LEGACY::default(),
                Cryptography::CERT_SYSTEM_STORE_LOCAL_MACHINE_ID
                    << Cryptography::CERT_SYSTEM_STORE_LOCATION_SHIFT,
                data.as_ptr() as *mut _,
            );
            if !store.is_null() {
                Ok(CertStore(store))
            } else {
                Err(io::Error::last_os_error())
            }
        }
    }

    /// Imports a PKCS#12-encoded key/certificate pair, returned as a
    /// `CertStore` instance.
    ///
    /// The password must also be provided to decrypt the encoded data.
    pub fn import_pkcs12(data: &[u8], password: Option<&str>) -> io::Result<CertStore> {
        unsafe {
            let blob = Cryptography::CRYPTOAPI_BLOB {
                cbData: data.len() as u32,
                pbData: data.as_ptr() as *mut u8,
            };
            let password = password.map(|s| {
                OsStr::new(s)
                    .encode_wide()
                    .chain(Some(0))
                    .collect::<Vec<_>>()
            });
            let password = password.as_ref().map(|s| s.as_ptr());
            let password = password.unwrap_or(ptr::null());
            let res = Cryptography::PFXImportCertStore(
                &blob,
                password,
                Cryptography::CRYPT_KEY_FLAGS::default(),
            );
            if !res.is_null() {
                Ok(CertStore(res))
            } else {
                Err(io::Error::last_os_error())
            }
        }
    }

    /// Returns an iterator over the certificates in this certificate store.
    pub fn certs(&self) -> Certs {
        Certs {
            store: self,
            cur: None,
        }
    }

    /// Adds a certificate context to this store.
    ///
    /// This function will add the certificate specified in `cx` to this store.
    /// A copy of the added certificate is returned.
    pub fn add_cert(&mut self, cx: &CertContext, how: CertAdd) -> io::Result<CertContext> {
        unsafe {
            let how = how as u32;
            let mut ret = ptr::null_mut();
            let res = Cryptography::CertAddCertificateContextToStore(
                self.0,
                cx.as_inner(),
                how,
                &mut ret,
            );
            if res == 0 {
                Err(io::Error::last_os_error())
            } else {
                Ok(CertContext::from_inner(ret))
            }
        }
    }

    /// Exports this certificate store as a PKCS#12-encoded blob.
    ///
    /// The password specified will be the password used to unlock the returned
    /// data.
    pub fn export_pkcs12(&self, password: &str) -> io::Result<Vec<u8>> {
        unsafe {
            let password = password.encode_utf16().chain(Some(0)).collect::<Vec<_>>();
            let mut blob = mem::zeroed();
            let res = Cryptography::PFXExportCertStore(
                self.0,
                &mut blob,
                password.as_ptr(),
                Cryptography::EXPORT_PRIVATE_KEYS,
            );
            if res == 0 {
                return Err(io::Error::last_os_error());
            }
            let mut ret = Vec::with_capacity(blob.cbData as usize);
            blob.pbData = ret.as_mut_ptr();
            let res = Cryptography::PFXExportCertStore(
                self.0,
                &mut blob,
                password.as_ptr(),
                Cryptography::EXPORT_PRIVATE_KEYS,
            );
            if res == 0 {
                return Err(io::Error::last_os_error());
            }
            ret.set_len(blob.cbData as usize);
            Ok(ret)
        }
    }
}

/// An iterator over the certificates contained in a `CertStore`, returned by
/// `CertStore::iter`
pub struct Certs<'a> {
    store: &'a CertStore,
    cur: Option<CertContext>,
}

impl<'a> Iterator for Certs<'a> {
    type Item = CertContext;

    fn next(&mut self) -> Option<CertContext> {
        unsafe {
            let cur = self.cur.take().map(|p| {
                let ptr = p.as_inner();
                mem::forget(p);
                ptr
            });
            let cur = cur.unwrap_or(ptr::null_mut());
            let next = Cryptography::CertEnumCertificatesInStore(self.store.0, cur);

            if next.is_null() {
                self.cur = None;
                None
            } else {
                let next = CertContext::from_inner(next);
                self.cur = Some(next.clone());
                Some(next)
            }
        }
    }
}

/// A builder type for imports of PKCS #12 archives.
#[derive(Default)]
pub struct PfxImportOptions {
    password: Option<Vec<u16>>,
    flags: u32,
}

impl PfxImportOptions {
    /// Returns a new `PfxImportOptions` with default settings.
    pub fn new() -> PfxImportOptions {
        PfxImportOptions::default()
    }

    /// Sets the password to be used to decrypt the archive.
    pub fn password(&mut self, password: &str) -> &mut PfxImportOptions {
        self.password = Some(password.encode_utf16().chain(Some(0)).collect());
        self
    }

    /// If set, the private key in the archive will not be persisted.
    ///
    /// If not set, private keys are persisted on disk and must be manually deleted.
    pub fn no_persist_key(&mut self, no_persist_key: bool) -> &mut PfxImportOptions {
        self.flag(Cryptography::PKCS12_NO_PERSIST_KEY, no_persist_key)
    }

    /// If set, all extended properties of the certificate will be imported.
    pub fn include_extended_properties(
        &mut self,
        include_extended_properties: bool,
    ) -> &mut PfxImportOptions {
        self.flag(
            Cryptography::PKCS12_INCLUDE_EXTENDED_PROPERTIES,
            include_extended_properties,
        )
    }

    fn flag(&mut self, flag: u32, set: bool) -> &mut PfxImportOptions {
        if set {
            self.flags |= flag;
        } else {
            self.flags &= !flag;
        }
        self
    }

    /// Imports certificates from a PKCS #12 archive, returning a `CertStore` containing them.
    pub fn import(&self, data: &[u8]) -> io::Result<CertStore> {
        unsafe {
            let blob = Cryptography::CRYPTOAPI_BLOB {
                cbData: cmp::min(data.len(), u32::max_value() as usize) as u32,
                pbData: data.as_ptr() as *mut _,
            };
            let password = self.password.as_ref().map_or(ptr::null(), |p| p.as_ptr());

            let store = Cryptography::PFXImportCertStore(&blob, password, self.flags);
            if !store.is_null() {
                Ok(CertStore(store))
            } else {
                Err(io::Error::last_os_error())
            }
        }
    }
}

/// Representation of an in-memory certificate store.
///
/// Internally this contains a `CertStore` which this type can be converted to.
pub struct Memory(CertStore);

impl Memory {
    /// Creates a new in-memory certificate store which certificates and CTLs
    /// can be added to.
    ///
    /// Initially the returned certificate store contains no certificates.
    pub fn new() -> io::Result<Memory> {
        unsafe {
            let store = Cryptography::CertOpenStore(
                Cryptography::CERT_STORE_PROV_MEMORY,
                Cryptography::CERT_QUERY_ENCODING_TYPE::default(),
                Cryptography::HCRYPTPROV_LEGACY::default(),
                Cryptography::CERT_OPEN_STORE_FLAGS::default(),
                ptr::null_mut(),
            );
            if !store.is_null() {
                Ok(Memory(CertStore(store)))
            } else {
                Err(io::Error::last_os_error())
            }
        }
    }

    /// Adds a new certificate to this memory store.
    ///
    /// For example the bytes could be a DER-encoded certificate.
    pub fn add_encoded_certificate(&mut self, cert: &[u8]) -> io::Result<CertContext> {
        unsafe {
            let mut cert_context = ptr::null_mut();

            let res = Cryptography::CertAddEncodedCertificateToStore(
                (self.0).0,
                Cryptography::X509_ASN_ENCODING | Cryptography::PKCS_7_ASN_ENCODING,
                cert.as_ptr(),
                cert.len() as u32,
                Cryptography::CERT_STORE_ADD_ALWAYS,
                &mut cert_context,
            );
            if res != 0 {
                Ok(CertContext::from_inner(cert_context))
            } else {
                Err(io::Error::last_os_error())
            }
        }
    }

    /// Adds a new CTL to this memory store, in its encoded form.
    ///
    /// This can be created through the `ctl_context::Builder` type.
    pub fn add_encoded_ctl(&mut self, ctl: &[u8]) -> io::Result<CtlContext> {
        unsafe {
            let mut ctl_context = ptr::null_mut();

            let res = Cryptography::CertAddEncodedCTLToStore(
                (self.0).0,
                Cryptography::X509_ASN_ENCODING | Cryptography::PKCS_7_ASN_ENCODING,
                ctl.as_ptr(),
                ctl.len() as u32,
                Cryptography::CERT_STORE_ADD_ALWAYS,
                &mut ctl_context,
            );
            if res != 0 {
                Ok(CtlContext::from_inner(ctl_context))
            } else {
                Err(io::Error::last_os_error())
            }
        }
    }

    /// Consumes this memory store, returning the underlying `CertStore`.
    pub fn into_store(self) -> CertStore {
        self.0
    }
}

#[cfg(test)]
mod test {
    use crate::ctl_context::CtlContext;

    use super::*;

    #[test]
    fn load() {
        let cert = include_bytes!("../test/cert.der");
        let mut store = Memory::new().unwrap();
        store.add_encoded_certificate(cert).unwrap();
    }

    #[test]
    fn create_ctl() {
        let cert = include_bytes!("../test/self-signed.badssl.com.cer");
        let mut store = Memory::new().unwrap();
        let cert = store.add_encoded_certificate(cert).unwrap();

        CtlContext::builder()
            .certificate(cert)
            .usage("1.3.6.1.4.1.311.2.2.2")
            .encode_and_sign()
            .unwrap();
    }

    #[test]
    fn pfx_import() {
        let pfx = include_bytes!("../test/identity.p12");
        let store = PfxImportOptions::new()
            .include_extended_properties(true)
            .password("mypass")
            .import(pfx)
            .unwrap();
        assert_eq!(store.certs().count(), 2);
        let pkeys = store
            .certs()
            .filter(|c| {
                c.private_key()
                    .compare_key(true)
                    .silent(true)
                    .acquire()
                    .is_ok()
            })
            .count();
        assert_eq!(pkeys, 1);
    }
}
