Source code for pycryptoki.cryptoki.helpers

"""
Contains some helper functions for creating C Bindings.
"""
import logging
import os
import re
import struct
import sys
from ctypes import CDLL

from six.moves import configparser

from pycryptoki.cryptoki.ck_defs import CK_RV
from pycryptoki.defaults import CHRYSTOKI_DLL_FILE, CHRYSTOKI_CONFIG_FILE
from pycryptoki.exceptions import LunaException

IS_WINDOWS = "win" in sys.platform


def struct_def(struct, fields):
    """
    Defines the fields of a given structure as specified.

    Checks if the system is Windows first, as that would need a different struct packing.

    :param struct: Class definition of a struct.
    :param fields: List of tuples defining the fields (see ctypes docs)
    """
    if IS_WINDOWS:
        struct._pack_ = 1
    struct._fields_ = fields


LOG = logging.getLogger(__name__)
IS_64B = 8 * struct.calcsize("P") == 64
CRYSTOKI_CONF_DLL = "CHRYSTOKI_CONF_DLL"


class CryptokiConfigException(LunaException):
    """
    Exception raised when we fail to determine the PKCS11 library location
    """

    pass


def parse_chrystoki_conf():
    """Parse the crystoki.ini/Chrystoki.conf file to find the library .so/.dll file so that
    we can use it.
    """

    env_conf_path = os.environ.get("ChrystokiConfigurationPath")
    conf_path = None
    if CHRYSTOKI_DLL_FILE is not None:
        # Use this value for the location of the DLL
        dll_path = CHRYSTOKI_DLL_FILE
        LOG.debug("Using DLL Path from defaults.py: %s", dll_path)
        return dll_path
    elif CHRYSTOKI_CONFIG_FILE is not None:
        conf_path = CHRYSTOKI_CONFIG_FILE
        LOG.debug("Using Chrystoki.conf location from defaults.py: %s", conf_path)
    elif env_conf_path is not None:
        if "win" in sys.platform:
            env_conf_path = env_conf_path.replace("\\\\", "~").replace("~", "\\") + "crystoki.ini"
        else:
            env_conf_path = os.path.join(env_conf_path, "Chrystoki.conf")
        conf_path = env_conf_path

        LOG.debug(
            "Using Chrystoki.conf location from environment variable "
            "ChrystokiConfigurationPath: %s",
            conf_path,
        )

    if conf_path is None:
        conf_path = "/etc/Chrystoki.conf"
        LOG.warning(
            "No DLL Path or Chyrstoki.conf path set in defaults.py " "looking up DLL path in %s",
            conf_path,
        )

    LOG.debug("Searching %s for Chrystoki DLL path...", conf_path)

    dll_path = _search_for_dll_in_chrystoki_conf(conf_path)

    LOG.info("Using DLL at location: %s", dll_path)

    return dll_path


def _search_for_dll_in_chrystoki_conf(conf_path):
    """Parses the chrystoki configuration file for the section that specifies the location
    of the DLL and returns the DLL location.

    :param str conf_path: The path to the configuration file
    :returns: The path to the chrystoki DLL
    :rtype: str
    """
    if "win" in sys.platform:
        try:
            config = configparser.ConfigParser()
            config.read(conf_path)

            dll_path = config.get("Chrystoki2", "LibNT")
        except ValueError:
            LOG.exception("Failed to read DLL from crystoki.ini.")
            raise CryptokiConfigException("Failed to read DLL location crystoki.ini file!")
        else:
            if not os.path.isfile(dll_path):
                raise CryptokiConfigException(
                    "Cryptoki DLL does not exist at path {}! Check your "
                    "crystoki.ini file.".format(dll_path)
                )
    else:
        with open(conf_path) as conf_file:
            chrystoki_conf_text = conf_file.read()
        chrystoki2_segments = re.findall(r"\s*Chrystoki2\s*=\s*\{([^\}]*)", chrystoki_conf_text)

        if len(chrystoki2_segments) > 1:
            raise CryptokiConfigException(
                "Found %d Chrystoki2 sections in the config file: "
                "%s" % (len(chrystoki2_segments), conf_path)
            )
        elif len(chrystoki2_segments) < 1:
            raise CryptokiConfigException(
                "Found no Chrystoki2 section in the config file:" " %s" % conf_path
            )

        chrystoki2 = chrystoki2_segments[0].split("\n")
        dll_path = ""
        for line in chrystoki2:
            is_64bits = sys.maxsize > 2 ** 32
            if is_64bits:
                lib_unix_line = re.findall(r"^\s*Lib(?:UNIX64|HPUX)\s*=\s*([^\n]+)", line)
            else:
                lib_unix_line = re.findall(r"^\s*Lib(?:UNIX|HPUX)\s*=\s*([^\n]+)", line)

            if len(lib_unix_line) > 1:
                raise CryptokiConfigException(
                    "Found more than one" " LibUNIX pattern on the same line"
                )
            elif len(lib_unix_line) == 1:
                if dll_path != "":
                    raise CryptokiConfigException(
                        "Found more than one instance of" " LibUNIX in the file."
                    )
                dll_path = lib_unix_line[0].strip().strip(";").strip().strip("'").strip('"')

        if dll_path == "":
            raise CryptokiConfigException(
                "Error finding LibUNIX declaration in configuration file:" " %s" % conf_path
            )

    return dll_path


class CryptokiDLLException(Exception):
    """Custom exception class used to print an error when a call to the Cryptoki DLL failed.
    The late binding makes debugging a little bit more difficult because function calls
    have to pass through an additional layer of abstraction. This custom exception prints
    out a quick message detailing exactly what function failed.


    """

    def __init__(self, additional_info, orig_error):
        self.msg = additional_info
        self.original_error = orig_error

    def __str__(self):
        return self.msg + "\n" + str(self.original_error)


class CryptokiDLLSingleton(object):
    """A singleton class which holds an instance of the loaded cryptoki DLL object."""

    _instance_map = {}
    loaded_dll_library = None

    def __new__(cls, *args, **kwargs):
        if not cls._instance_map.get(CRYSTOKI_CONF_DLL):
            new_instance = super(CryptokiDLLSingleton, cls).__new__(cls, *args, **kwargs)

            dll_path = parse_chrystoki_conf()
            new_instance.dll_path = dll_path
            if "win" in sys.platform and IS_64B:
                import ctypes

                new_instance.loaded_dll_library = ctypes.WinDLL(dll_path)
            else:
                new_instance.loaded_dll_library = CDLL(dll_path)
            cls._instance_map[CRYSTOKI_CONF_DLL] = new_instance
        return cls._instance_map[CRYSTOKI_CONF_DLL]

    def get_dll(self):
        """Get the loaded library (parsed from crystoki.ini/Chrystoki.conf)"""
        if self.loaded_dll_library is None or self.loaded_dll_library == "":
            raise CryptokiConfigException(
                "DLL path not found:\n"
                "1. Is the Luna HSM Client installed?\n"
                "2. Can python read the Luna HSM Client config file?\n"
                "3. Is there a LibUNIX/LibNT field in the Luna HSM Client config file"
            )
        return self.loaded_dll_library

    @classmethod
    def from_path(cls, path):
        if not cls._instance_map.get(path):
            new_instance = super(CryptokiDLLSingleton, cls).__new__(cls)
            cls._instance_map[path] = new_instance
            new_instance.dll_path = path
            if "win" in sys.platform and IS_64B:
                import ctypes

                new_instance.loaded_dll_library = ctypes.WinDLL(path)
            else:
                new_instance.loaded_dll_library = CDLL(path)
        return cls._instance_map[path]


def log_args(funcname, args):
    """Log function name & arguments for a cryptoki ctypes call.

    :param str funcname: Function name
    :param tuple args: Arguments to be passed to ctypes function.
    """
    log_msg = "Cryptoki call: {}({})".format(funcname, ", ".join(str(arg) for arg in args))
    LOG.debug(log_msg)


def make_late_binding_function(function_name, argtypes=None, restype=CK_RV):
    """A function factory for creating a function that will bind to the cryptoki
    DLL only when the function is called.

    :param function_name: Name of the function in the DLL.
    :return: Late bound function.
    """

    def luna_function(*args):
        """
        Placeholder
        """
        late_binded_function = getattr(CryptokiDLLSingleton().get_dll(), function_name)
        late_binded_function.restype = luna_function.restype if restype is None else restype
        late_binded_function.argtypes = luna_function.argtypes if argtypes is None else argtypes

        log_args(function_name, args)
        try:
            return_value = late_binded_function(*args)
            return return_value
        except Exception as e:
            raise CryptokiDLLException(
                "Call to '{}({})' "
                "failed.".format(function_name, ", ".join([str(arg) for arg in args])),
                e,
            )

    luna_function.__name__ = function_name
    luna_function.__doc__ = """Cryptoki DLL call to {}.

{}

    :return: {}
    """.format(
        function_name,
        "\n".join(
            "    :param arg{}: {}".format(i, x.__name__)
            for i, x in enumerate(argtypes, 1)
            if x is not None
        )
        if argtypes
        else "unknown",
        restype.__name__,
    )
    return luna_function