/*! @file

    @id $Id$
*/
//       1         2         3         4         5         6         7         8
// 45678901234567890123456789012345678901234567890123456789012345678901234567890

#include <cryptoki.hxx>

#include <sstream>
#include <mrw/checkcxx11.hxx>
#include <memory>
#ifndef WIN32
#include <dlfcn.h>
#define CK_PTR *
typedef CK_FUNCTION_LIST CK_PTR CK_FUNCTION_LIST_PTR;
typedef CK_FUNCTION_LIST_PTR CK_PTR CK_FUNCTION_LIST_PTR_PTR;
typedef CK_RV (*CK_C_GetFunctionList)
    (CK_FUNCTION_LIST_PTR_PTR ppFunctionList);
#else
#include <windows.h>
#undef ERROR
#endif

namespace cryptoki {

  bool Library::Init::functionList(const std::string& library) {
    CRYPTOLOG("try to load: "<<library);
    #ifndef WIN32
    _lib = dlopen(library.c_str(), RTLD_NOW);
    #else
    _lib = LoadLibrary(library.c_str());
    #endif
    if (!_lib) throw exception("open of library failed: "+library);
    CRYPTOLOG("loaded: "<<library);
    #ifndef WIN32
    /// @bug in  dlsym: returns void* but should return void(*)()
    CK_C_GetFunctionList fnl
      (reinterpret_cast<CK_C_GetFunctionList>
       (reinterpret_cast<long long>
        (dlsym(_lib, "C_GetFunctionList"))));
    #else
    CK_C_GetFunctionList fnl
      ((CK_C_GetFunctionList)GetProcAddress(_lib, "C_GetFunctionList"));
    #endif
    if (!fnl)
      throw exception("required library symbol C_GetFunctionList not found in "
                      +library);
    CRYPTOLOG("Got C_GetFunctionList, now call it");
    //! calls @c C_GetFunctionList
    return check(fnl(&_fn), CRYPTOKI_FN_LOG("C_GetFunctionList"));
  }

  bool Library::Init::check(CK_RV result, const std::string& context) {
    _res = result;
    if (_exc && !*this) {
      if (context.size()) {
        if (_res==CKR_PIN_INCORRECT)
          throw wrong_pin(context+": "+error());
        else
          throw access_error(context+": "+error());
      } else {
        if (_res==CKR_PIN_INCORRECT)
          throw wrong_pin(error());
        else
          throw access_error(error());
      }
    }
    return _res==CKR_OK;
  }
  
  std::string Library::Init::error(CK_RV res) {
    switch (res) {
      case CKR_OK: return "CKR_OK";
      case CKR_CANCEL: return "CKR_CANCEL";
      case CKR_HOST_MEMORY: return "CKR_HOST_MEMORY";
      case CKR_SLOT_ID_INVALID: return "CKR_SLOT_ID_INVALID";
      case CKR_GENERAL_ERROR: return "CKR_GENERAL_ERROR";
      case CKR_FUNCTION_FAILED: return "CKR_FUNCTION_FAILED";
      case CKR_ARGUMENTS_BAD: return "CKR_ARGUMENTS_BAD";
      case CKR_NO_EVENT: return "CKR_NO_EVENT";
      case CKR_NEED_TO_CREATE_THREADS: return "CKR_NEED_TO_CREATE_THREADS";
      case CKR_CANT_LOCK: return "CKR_CANT_LOCK";
      case CKR_ATTRIBUTE_READ_ONLY: return "CKR_ATTRIBUTE_READ_ONLY";
      case CKR_ATTRIBUTE_SENSITIVE: return "CKR_ATTRIBUTE_SENSITIVE";
      case CKR_ATTRIBUTE_TYPE_INVALID: return "CKR_ATTRIBUTE_TYPE_INVALID";
      case CKR_ATTRIBUTE_VALUE_INVALID: return "CKR_ATTRIBUTE_VALUE_INVALID";
      case CKR_DATA_INVALID: return "CKR_DATA_INVALID";
      case CKR_DATA_LEN_RANGE: return "CKR_DATA_LEN_RANGE";
      case CKR_DEVICE_ERROR: return "CKR_DEVICE_ERROR";
      case CKR_DEVICE_MEMORY: return "CKR_DEVICE_MEMORY";
      case CKR_DEVICE_REMOVED: return "CKR_DEVICE_REMOVED";
      case CKR_ENCRYPTED_DATA_INVALID: return "CKR_ENCRYPTED_DATA_INVALID";
      case CKR_ENCRYPTED_DATA_LEN_RANGE: return "CKR_ENCRYPTED_DATA_LEN_RANGE";
      case CKR_FUNCTION_CANCELED: return "CKR_FUNCTION_CANCELED";
      case CKR_FUNCTION_NOT_PARALLEL: return "CKR_FUNCTION_NOT_PARALLEL";
      case CKR_FUNCTION_NOT_SUPPORTED: return "CKR_FUNCTION_NOT_SUPPORTED";
      case CKR_KEY_HANDLE_INVALID: return "CKR_KEY_HANDLE_INVALID";
      case CKR_KEY_SIZE_RANGE: return "CKR_KEY_SIZE_RANGE";
      case CKR_KEY_TYPE_INCONSISTENT: return "CKR_KEY_TYPE_INCONSISTENT";
      case CKR_KEY_NOT_NEEDED: return "CKR_KEY_NOT_NEEDED";
      case CKR_KEY_CHANGED: return "CKR_KEY_CHANGED";
      case CKR_KEY_NEEDED: return "CKR_KEY_NEEDED";
      case CKR_KEY_INDIGESTIBLE: return "CKR_KEY_INDIGESTIBLE";
      case CKR_KEY_FUNCTION_NOT_PERMITTED:
        return "CKR_KEY_FUNCTION_NOT_PERMITTED";
      case CKR_KEY_NOT_WRAPPABLE: return "CKR_KEY_NOT_WRAPPABLE";
      case CKR_KEY_UNEXTRACTABLE: return "CKR_KEY_UNEXTRACTABLE";
      case CKR_MECHANISM_INVALID: return "CKR_MECHANISM_INVALID";
      case CKR_MECHANISM_PARAM_INVALID: return "CKR_MECHANISM_PARAM_INVALID";
      case CKR_OBJECT_HANDLE_INVALID: return "CKR_OBJECT_HANDLE_INVALID";
      case CKR_OPERATION_ACTIVE: return "CKR_OPERATION_ACTIVE";
      case CKR_OPERATION_NOT_INITIALIZED:
        return "CKR_OPERATION_NOT_INITIALIZED";
      case CKR_PIN_INCORRECT: return "CKR_PIN_INCORRECT";
      case CKR_PIN_INVALID: return "CKR_PIN_INVALID";
      case CKR_PIN_LEN_RANGE: return "CKR_PIN_LEN_RANGE";
      case CKR_PIN_EXPIRED: return "CKR_PIN_EXPIRED";
      case CKR_PIN_LOCKED: return "CKR_PIN_LOCKED";
      case CKR_SESSION_CLOSED: return "CKR_SESSION_CLOSED";
      case CKR_SESSION_COUNT: return "CKR_SESSION_COUNT";
      case CKR_SESSION_HANDLE_INVALID: return "CKR_SESSION_HANDLE_INVALID";
      case CKR_SESSION_PARALLEL_NOT_SUPPORTED:
        return "CKR_SESSION_PARALLEL_NOT_SUPPORTED";
      case CKR_SESSION_READ_ONLY: return "CKR_SESSION_READ_ONLY";
      case CKR_SESSION_EXISTS: return "CKR_SESSION_EXISTS";
      case CKR_SESSION_READ_ONLY_EXISTS: return "CKR_SESSION_READ_ONLY_EXISTS";
      case CKR_SESSION_READ_WRITE_SO_EXISTS:
        return "CKR_SESSION_READ_WRITE_SO_EXISTS";
      case CKR_SIGNATURE_INVALID: return "CKR_SIGNATURE_INVALID";
      case CKR_SIGNATURE_LEN_RANGE: return "CKR_SIGNATURE_LEN_RANGE";
      case CKR_TEMPLATE_INCOMPLETE: return "CKR_TEMPLATE_INCOMPLETE";
      case CKR_TEMPLATE_INCONSISTENT: return "CKR_TEMPLATE_INCONSISTENT";
      case CKR_TOKEN_NOT_PRESENT: return "CKR_TOKEN_NOT_PRESENT";
      case CKR_TOKEN_NOT_RECOGNIZED: return "CKR_TOKEN_NOT_RECOGNIZED";
      case CKR_TOKEN_WRITE_PROTECTED: return "CKR_TOKEN_WRITE_PROTECTED";
      case CKR_UNWRAPPING_KEY_HANDLE_INVALID:
        return "CKR_UNWRAPPING_KEY_HANDLE_INVALID";
      case CKR_UNWRAPPING_KEY_SIZE_RANGE:
        return "CKR_UNWRAPPING_KEY_SIZE_RANGE";
      case CKR_UNWRAPPING_KEY_TYPE_INCONSISTENT:
        return "CKR_UNWRAPPING_KEY_TYPE_INCONSISTENT";
      case CKR_USER_ALREADY_LOGGED_IN: return "CKR_USER_ALREADY_LOGGED_IN";
      case CKR_USER_NOT_LOGGED_IN: return "CKR_USER_NOT_LOGGED_IN";
      case CKR_USER_PIN_NOT_INITIALIZED: return "CKR_USER_PIN_NOT_INITIALIZED";
      case CKR_USER_TYPE_INVALID: return "CKR_USER_TYPE_INVALID";
      case CKR_USER_ANOTHER_ALREADY_LOGGED_IN:
        return "CKR_USER_ANOTHER_ALREADY_LOGGED_IN";
      case CKR_USER_TOO_MANY_TYPES: return "CKR_USER_TOO_MANY_TYPES";
      case CKR_WRAPPED_KEY_INVALID: return "CKR_WRAPPED_KEY_INVALID";
      case CKR_WRAPPED_KEY_LEN_RANGE: return "CKR_WRAPPED_KEY_LEN_RANGE";
      case CKR_WRAPPING_KEY_HANDLE_INVALID:
        return "CKR_WRAPPING_KEY_HANDLE_INVALID";
      case CKR_WRAPPING_KEY_SIZE_RANGE: return "CKR_WRAPPING_KEY_SIZE_RANGE";
      case CKR_WRAPPING_KEY_TYPE_INCONSISTENT:
        return "CKR_WRAPPING_KEY_TYPE_INCONSISTENT";
      case CKR_RANDOM_SEED_NOT_SUPPORTED:
        return "CKR_RANDOM_SEED_NOT_SUPPORTED";
      case CKR_RANDOM_NO_RNG: return "CKR_RANDOM_NO_RNG";
      case CKR_DOMAIN_PARAMS_INVALID: return "CKR_DOMAIN_PARAMS_INVALID";
      case CKR_BUFFER_TOO_SMALL: return "CKR_BUFFER_TOO_SMALL";
      case CKR_SAVED_STATE_INVALID: return "CKR_SAVED_STATE_INVALID";
      case CKR_INFORMATION_SENSITIVE: return "CKR_INFORMATION_SENSITIVE";
      case CKR_STATE_UNSAVEABLE: return "CKR_STATE_UNSAVEABLE";
      case CKR_CRYPTOKI_NOT_INITIALIZED: return "CKR_CRYPTOKI_NOT_INITIALIZED";
      case CKR_CRYPTOKI_ALREADY_INITIALIZED:
        return "CKR_CRYPTOKI_ALREADY_INITIALIZED";
      case CKR_MUTEX_BAD: return "CKR_MUTEX_BAD";
      case CKR_MUTEX_NOT_LOCKED: return "CKR_MUTEX_NOT_LOCKED";
      case CKR_VENDOR_DEFINED: return "CKR_VENDOR_DEFINED";
      default: {
        std::stringstream ss;
        ss<<"unknown error code ("<<res<<')';
        return ss.str();
      }
    }
  }
        
  Library::Init::Init(const std::string& library, bool exc) try:
    _exc(exc), _res(CKR_OK), _fn(0) {
      CRYPTOLOG("library: "<<library);
      //! calls @c functionList
      if (!functionList(library)) return;
      CRYPTOLOG("now initialize "<<library);
      assert(_fn);
      //! calls @c C_Initialize
      check(_fn->C_Initialize(0), //! @todo add optional argument
            CRYPTOKI_FN_LOG("C_Initialize"));
    } catch (...) {
    throw access_error(CRYPTOKI_FN_LOG("C_Initialize")
                       +": Error in initialization of library "+library);
  }

  Library::Init::~Init() {
    CRYPTOLOG("log");
    /// ignore failure in disconnect,  possibly smartcard has
    /// been removed
    try {
      //! calls @c C_Finalize
      check(_fn->C_Finalize(0), CRYPTOKI_FN_LOG("C_Finalize"));
#ifndef WIN32
      if (_lib) dlclose(_lib);
      _lib = 0;
#else
      FreeLibrary(_lib);
      _lib = 0;
#endif
      _fn = 0;
    } catch (std::exception& e) {
      CRYPTOLOG("unloading cryptoki library failed, reason: "+e.what());
    } catch (...) {
      CRYPTOLOG("unloading cryptoki library failed");
    }
  }
  
  Library::Init::operator bool() {
    return _res==CKR_OK;
  }

  std::string Library::Init::error() {
    return error(_res);
  }

  /*! @todo Not implemented:
      @code
      bool Library::Init::getinfo() {
      //! calls @c C_GetInfo
      return check(_init._fn->C_GetInfo(CK_INFO_PTR),
      CRYPTOKI_FN_LOG("C_GetInfo"));
      }
      @endcode */
        
  SlotList Library::slotList(bool tokenPresent, std::string name) {
    CRYPTOLOG("log");
    CRYPTOLOG("looking for card name: \""
              <<name<<(tokenPresent?"\" with token":""));
    SlotList res;
    CK_ULONG count(0);
    //! calls @c C_GetSlotList
    _init->check(_init->_fn->C_GetSlotList(tokenPresent?TRUE:FALSE, 0, &count),
                 CRYPTOKI_FN_LOG("C_GetSlotList"));
    CRYPTOLOG("found "<<count<<" readers, result: "<<(*this?"success":"error"));
    if (!count || !*this) return res;
    CK_SLOT_ID* slots = 0;
    try {
      CK_RV r(0);
      do {
        delete[] slots;
        slots = new CK_SLOT_ID[count];
        r = _init->_fn->C_GetSlotList(tokenPresent?TRUE:FALSE, slots, &count);
      } while (r==CKR_BUFFER_TOO_SMALL);
      _init->check(r, CRYPTOKI_FN_LOG("C_GetSlotList"));
      if (!*this) return res;
      for (CK_ULONG i(0); i<count; ++i) {
        Slot s(*this, slots[i]);
        CRYPTOLOG("found slot \""<<s.slotinfo().slotDescription<<"\"");
        if (!name.size() || name==s.slotinfo().slotDescription) {
          CRYPTOLOG("-> slot matches");
          res.push_back(s);
        }
      }
    } catch (...) {
      delete[] slots;
      throw;
    }
    delete[] slots;
    return res;
  }

  //============================================================================

  ObjectList Session::find(const AttributeList& attrs) {
    CRYPTOLOG("log");
    ObjectList res;
    CK_ATTRIBUTE* a(0);
    try {
      if (attrs.size()) {
        a = new CK_ATTRIBUTE[attrs.size()];
        for (AttributeList::size_type i(0); i<attrs.size(); ++i)
          a[i] = attrs[i];
      }
      //! calls @c C_FindObjectsInit
      if (check(_slot._library->C_FindObjectsInit
                (_session, a, attrs.size()),
                CRYPTOKI_FN_LOG("C_FindObjectsInit"))) {
        CK_OBJECT_HANDLE obj;
        //! calls @c C_FindObjects
        for (CK_ULONG objs(0);
             check(_slot._library->C_FindObjects
                   (_session, &obj, 1, &objs),
                   CRYPTOKI_FN_LOG("C_FindObjects")) && objs;
             res.push_back(Object(*this, obj)));
      }
      //! calls @c C_FindObjectsFinal
      check(_slot._library->C_FindObjectsFinal(_session),
            CRYPTOKI_FN_LOG("C_FindObjectsFinal"));
      delete[] a;
      return res;
    } catch (...) {
      delete[] a;
      throw;
    }
  }
  
  //----------------------------------------------------------------------------
  ObjectList Session::find(const Attribute& a) {
    CRYPTOLOG("log");
    AttributeList al;
    al.push_back(a);
    return find(al);
  }
  
  //----------------------------------------------------------------------------
  ObjectList Session::find(const Attribute& a1, const Attribute& a2) {
    CRYPTOLOG("log");
    AttributeList al;
    al.push_back(a1);
    al.push_back(a2);
    return find(al);
  }

  //----------------------------------------------------------------------------
  Object Session::create(const std::string& label, const openssl::X509& cert) {
    CRYPTOLOG("log");
    AttributeList attrs;
    attrs.push_back(Attribute(CKA_CLASS)
                    .from<CK_OBJECT_CLASS>(CKO_CERTIFICATE));
    attrs.push_back(Attribute(CKA_TOKEN).from<CK_BBOOL>(TRUE));
    attrs.push_back(Attribute(CKA_PRIVATE).from<CK_BBOOL>(FALSE));
    attrs.push_back(Attribute(CKA_MODIFIABLE).from<CK_BBOOL>(TRUE));
    attrs.push_back(Attribute(CKA_LABEL, label));
    attrs.push_back(Attribute(CKA_CERTIFICATE_TYPE)
                    .from<CK_CERTIFICATE_TYPE>(CKC_X_509));
    attrs.push_back(Attribute(CKA_SUBJECT, cert.subjectDER()));
    attrs.push_back(Attribute(CKA_ID, cert.id()));
    attrs.push_back(Attribute(CKA_ISSUER, cert.issuerDER()));
    attrs.push_back(Attribute(CKA_SERIAL_NUMBER, cert.serial()));
    attrs.push_back(Attribute(CKA_VALUE, cert.valueDER()));
    CRYPTOLOG("create: serial = "<<crypto::hex(cert.serial()));
    return create(attrs);
  }
  
  Object Session::create(const std::string& label,
                         const openssl::PrivateKey& key,
                         const openssl::X509& cert) {
    CRYPTOLOG("log");
    int usage(cert.keyUsageFlags());
    AttributeList attrs;
    attrs.push_back(Attribute(CKA_CLASS)
                    .from<CK_OBJECT_CLASS>(CKO_PRIVATE_KEY));
    attrs.push_back(Attribute(CKA_TOKEN).from<CK_BBOOL>(TRUE));
    attrs.push_back(Attribute(CKA_PRIVATE).from<CK_BBOOL>(TRUE));
    attrs.push_back(Attribute(CKA_MODIFIABLE).from<CK_BBOOL>(TRUE));
    attrs.push_back(Attribute(CKA_LABEL, label));
    attrs.push_back(Attribute(CKA_KEY_TYPE).from<CK_KEY_TYPE>(CKK_RSA));
    attrs.push_back(Attribute(CKA_ID, cert.id()));
    attrs.push_back(Attribute(CKA_DERIVE).from<CK_BBOOL>(FALSE));
    attrs.push_back(Attribute(CKA_SUBJECT, cert.subjectDER()));
    attrs.push_back(Attribute(CKA_SENSITIVE).from<CK_BBOOL>(TRUE));
    attrs.push_back(Attribute(CKA_SECONDARY_AUTH).from<CK_BBOOL>(FALSE));
    attrs.push_back(Attribute(CKA_DECRYPT) // Required by Doujak/Inverardi
                    .from<CK_BBOOL>(usage&(X509v3_KU_DATA_ENCIPHERMENT
                                           |(usage&X509v3_KU_KEY_ENCIPHERMENT))
                                    ?TRUE:FALSE)); // instead of CKA_UNWRAP
    attrs.push_back(Attribute(CKA_SIGN)
                    .from<CK_BBOOL>(usage&(X509v3_KU_DIGITAL_SIGNATURE
                                           |X509v3_KU_NON_REPUDIATION)
                                    ?TRUE:FALSE));
    attrs.push_back(Attribute(CKA_SIGN_RECOVER) // same as CKA_SIGN
                    .from<CK_BBOOL>(usage&(X509v3_KU_DIGITAL_SIGNATURE
                                           |X509v3_KU_NON_REPUDIATION)
                                    ?TRUE:FALSE));
    attrs.push_back(Attribute(CKA_EXTRACTABLE).from<CK_BBOOL>(FALSE));
    attrs.push_back(Attribute(CKA_MODULUS, key.modulus()));
    attrs.push_back(Attribute(CKA_PUBLIC_EXPONENT, key.publicExponent()));
    attrs.push_back(Attribute(CKA_PRIVATE_EXPONENT, key.privateExponent()));
    attrs.push_back(Attribute(CKA_PRIME_1, key.prime1()));
    attrs.push_back(Attribute(CKA_PRIME_2, key.prime2()));
    attrs.push_back(Attribute(CKA_EXPONENT_1, key.exponent1()));
    attrs.push_back(Attribute(CKA_EXPONENT_2, key.exponent2()));
    attrs.push_back(Attribute(CKA_COEFFICIENT, key.coefficient()));
    return create(attrs);
  }
  
  //----------------------------------------------------------------------------
  Object Session::create(const AttributeList& attrs) {
    CRYPTOLOG("log");
    CK_ATTRIBUTE* a(0);
    try {
      if (attrs.size()) {
        a = new CK_ATTRIBUTE[attrs.size()];
        for (AttributeList::size_type i(0); i<attrs.size(); ++i)
          a[i] = attrs[i];
      }
      for (AttributeList::size_type i(0); i<attrs.size(); ++i) {
        std::string value((char*)a[i].pValue, a[i].ulValueLen);
        Attribute xxx(a[i].type, value);
        CRYPTOLOG("Attribute: "<<xxx.name()<<" = "<<xxx.readableValue());
      };
      CK_OBJECT_HANDLE object;
      //! calls @c C_CreateObject
      check(_slot._library->C_CreateObject
            (_session, a, attrs.size(), &object),
            CRYPTOKI_FN_LOG("C_CreateObject"));
      delete[] a;
      return Object(*this, object);
    } catch (...) {
      delete[] a;
      throw;
    }
  }

}
