/*! @file

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

#ifndef __SUISSEID_HXX__
#define __SUISSEID_HXX__

#include <cardos.hxx>
#include <cryptoki.hxx>
#include <pcsc.hxx>
#include <mrw/vector.hxx>
#include <mrw/checkcxx11.hxx>
#include <memory>

/*! @defgroup gsuisseid C++ library to access SuisseID smart cards

    This library allows access to the Swiss digital identity cards
    (SuisseID).

    You need to include @ref suisseid.hxx, then start with class @ref
    suisseid::Scanner to scan for a list of SuisseID cards on the system.

    @see http://www.suisseid.ch
    @see http://postsuisseid.ch

*/
//@{
/*! @defgroup suisseidlib SuisseID Library */
/*! @defgroup suisseidtypes SuisseID C++ Types and Auxiliary */
/*! @defgroup suisseidconsts SuisseID C++ Constants */
/*! @defgroup suisseidexceptions SuisseID Exceptions */
/** @example suisse-id-demo.cxx

    Usage of @ref gsuisseid This is a comprehensive example how you
    can access a SuisseID and access to certificates on that card.

    First implement a status cycle, here for @c std::cin and @c
    std::cout as user interface in the @c suisse-id-demo.hxx header
    file:

    @include suisse-id-demo.hxx

    Then instanciate and use this class from your code: */
//@}

/// @ref gsuisseid @copydoc gsuisseid
namespace suisseid {


  /** @page init Initialize Card and Check Status

      An idea on how the smart card status could be evaluated is the
      following state machine:

      @dot
      digraph {
        transportState
          [URL="\ref cardos::Commands::transportState"];
        transportPinRetries
          [URL="\ref cardos::Commands::transportPinRetries"];
        pkcs15PinRetries
          [URL="\ref cardos::Commands::pkcs15PinRetries"];
        sigGPinRetries
          [URL="\ref cardos::Commands::sigGPinRetries"];
        pukRetries
          [URL="\ref cardos::Commands::pukRetries"];
        changePin
          [URL="\ref cardos::Commands::changePins"];
        broken [label="replace card"];
      
        start ->  transportState;
        transportPinRetries -> broken [label="-1"];
        certsValid -> broken [label="false"];
        pukRetries -> broken [label="-1"];
        sigGPinRetries -> broken [label="-1"];
        transportState -> transportPinRetries [label="true"];
        transportPinRetries -> changePin [label=">-1"];
        changePin -> transportState;
        transportState -> haveCerts [label="false"];
        haveCerts -> installCerts [label="false"];
        installCerts -> transportState;
        haveCerts -> certsValid [label="true"];
        certsValid -> pkcs15PinRetries [label="true"];
        pkcs15PinRetries -> sigGPinRetries [label=">-1"];
        pkcs15PinRetries -> pukRetries [label="-1"];
        pukRetries -> changePin [label=">-1"];
        sigGPinRetries -> valid [label=">-1"];
        {valid broken} -> end;

        {rank=same; valid broken}
        {rank=same; transportPinRetries haveCerts}
        {rank=same; certsValid installCerts changePin}
      }
      @enddot

      */

  //============================================================================
  /*! @addtogroup suisseidexceptions */
  //@{
  
  //----------------------------------------------------------------------------
  class exception: public std::exception {
    public:
      exception(const std::string& reason) throw():
          _what("suisseid: "+reason) {
        CRYPTOLOG("ERROR: "<<what());
      }
      ~exception() throw() {}
      const char* what() const throw() {
        return _what.c_str();
      }
    private:
      std::string _what;
  };
  //----------------------------------------------------------------------------
  class no_certfound: public exception {
    public:
      no_certfound(const std::string& label) throw():
          exception("no certificate with label \""+label+"\" found") {}
  };
  //----------------------------------------------------------------------------
  class no_auth: public exception {
    public:
      no_auth() throw():
          exception("no authentication certificate found") {}
  };
  //----------------------------------------------------------------------------
  class no_digsig: public exception {
    public:
      no_digsig() throw():
          exception("no digital signature certificate found") {}
  };
  //----------------------------------------------------------------------------
  class slot_not_found: public exception {
    public:
      slot_not_found(const std::string& name) throw():
          exception("matching cryptoki slot for "+name+" not found") {}
  };
  //@}
  
  /*! @addtogroup suisseidtypes */
  //@{
  /// DER encoded binary certificate
  class Certificate: public std::string {
    public:
      Certificate(const std::string& v,
                  const std::string& l,
                  const cryptoki::Attribute& i):
          std::string(v),
          _label(l),
          _id(i) {
      }
      const std::string& label() {
        return _label;
      }
      const cryptoki::Attribute& id() {
        return _id;
      }
    private:
      std::string _label;
      cryptoki::Attribute _id;
  };
  
  /// List of DER encoded binary certificates
  typedef std::vector<Certificate> Certificates;
  //@}

  /*! @addtogroup suisseidconsts */
  //@{
  /// Label of the key for digital signature certificate.
  const std::string NON_REP  = "SwissSign_nonRep                ";
  /// Label of the key for authentication certificate
  const std::string DIG_SIG  = "SwissSign_digSig                ";
  /// Label of the key required for getting the certificates
  const std::string DATA_ENC = "SwissSign_dataEnc               ";
  //@}
  
  /*! @addtogroup suisseidlib */
  //@{
  
  //! Represents a SuisseID Card
  /*! This is the parent class for special classes for the respecive
      SuisseID providers. */
  class Card: public cardos::Commands {

    public:

      /// Status of the card's certificates
      /** @note by now, only @c MISSING and @c VALID is supported */
      enum CertStatus {
        MISSING,      ///< certificate is missing, needs initiatlization
        // EXPIRES_SOON, ///< certificate will soon expire, needs renewal
        // EXPIRED,      ///< certificate is expired, needs new purchase
        // REVOKED,      ///< certificate has been revoked and is invalid
        VALID         ///< certificate is valid
      };
      
    public:

      /// Instanciation is done within suisseid::Scanner
      /** Instance requires a connenction to the reader an a cryptoky
          library. This is passes automatically when this class is
          instanciated through suisseid::Scanner. */
      Card(std::shared_ptr<pcsc::Connection::Reader> reader,
           const cryptoki::Library& cryptoki):
          cardos::Commands(reader),
          _cryptoki(cryptoki) {
      }
      virtual ~Card() {}

      /// Find the matching cryptoki::Slot for further access
      /** @throws slot_not_found if not exactly one matching slot exists */
      cryptoki::Slot slot() {
        cryptoki::SlotList slots(_cryptoki.slotList(true, name()));
        if (slots.size()==1) return slots[0];
        throw slot_not_found(name());
      }

      /// Get the reader, needed for example to lock a transaction
      /** @code
          pcsc::Connection::Reader::Transaction lock(card.reader());
          [... do some low level stuff ...]
          @endcode */
      std::shared_ptr<pcsc::Connection::Reader> reader() {
        return _reader;
      }
      
      /// Minimum allowed PIN length for this card.
      virtual unsigned int minimalPinLength() = 0;
      /// Maximum allowed PIN length for this card.
      virtual unsigned int maximalPinLength() = 0;

      //! Name of the token/slot
      const std::string& name() {
        return _reader->name;
      }

      /// Version of the card
      virtual std::string version() {
        return "<unknown>";
      }
      
      /// Status of the certificates on the card
      virtual CertStatus certStatus() {
        return MISSING;
      }

      /// Starts and returns a cryptoki::Session.
      cryptoki::Session session() {
        return cryptoki::Session(slot());
      }

      /// Starts a cryptoki::Session and returns cryptoki::Session::Info.
      cryptoki::Session::Info sessionInfo() {
        return session().getsessioninfo();
      }

      /// @returns Certificates in DER format.
      Certificates certificates() {
        Certificates res;
        cryptoki::ObjectList certs
          (session().find(cryptoki::Attribute(CKA_CLASS)
                          .from<CK_OBJECT_CLASS>(CKO_CERTIFICATE)));
        for (cryptoki::ObjectList::iterator cert(certs.begin());
             cert!=certs.end(); ++cert)
          res.push_back(Certificate(cert->attribute(CKA_VALUE).value,
                                    cert->attribute(CKA_LABEL).value,
                                    cert->attribute(CKA_ID)));
        return res;
      }
      
      virtual Certificate certificate(const std::string& keylabel) {
#       ifdef __APPLE__
          /*! @bug Work around Mac OSX 10.10 bug. On Mac OSX 10.10
                   there is a bug in PCSC: After a reconnect, first
                   transaction (SCardTransmit) fails with
                   SCARD_W_RESET_CARD (0x80100068). */
          static int applebug(0);
#       endif
        CRYPTOLOG("get certificate for key "<<keylabel);
        cryptoki::ObjectList keys // find keys with digsig-label
          (session().find(cryptoki::AttributeList()
                          <<cryptoki::Attribute(CKA_CLASS)
                          .from<CK_OBJECT_CLASS>(CKO_PUBLIC_KEY)
                          <<cryptoki::Attribute(CKA_LABEL, keylabel)));
        CRYPTOLOG("found "<<keys.size()<<" keys");
        for (cryptoki::ObjectList::iterator key(keys.begin());
             key!=keys.end(); ++key) {
          cryptoki::Attribute id(key->attribute(CKA_ID));
          CRYPTOLOG("get certs for key with id "<<crypto::hex(id.value));
          cryptoki::ObjectList certs
            (session().find(cryptoki::AttributeList()
                            <<cryptoki::Attribute(CKA_CLASS)
                            .from<CK_OBJECT_CLASS>(CKO_CERTIFICATE)
                            <<id));
          CRYPTOLOG("found "<<certs.size()<<" certificates");
#       ifdef __APPLE__
          /*! @bug Work around Mac OSX 10.10 bug. On Mac OSX 10.10
                   there is a bug in PCSC: After a reconnect, first
                   transaction (SCardTransmit) fails with
                   SCARD_W_RESET_CARD (0x80100068). */
          try {
#       endif
            for (cryptoki::ObjectList::iterator cert(certs.begin());
                 cert!=certs.end(); ++cert) // return first matching cert
              return Certificate(cert->attribute(CKA_VALUE).value,
                                 cert->attribute(CKA_LABEL).value,
                                 id);
#         ifdef __APPLE__
            /*! @bug Work around Mac OSX 10.10 bug. On Mac OSX 10.10
                     there is a bug in PCSC: After a reconnect, first
                     transaction (SCardTransmit) fails with
                     SCARD_W_RESET_CARD (0x80100068). */
          } catch (std::exception& x) {
            if (++applebug>1) {
              CRYPTOLOG("failed again after apple bug retry with "<<x.what());
              applebug = 0; // failed again, give up, reset counter
              throw;
            } else {
              CRYPTOLOG("failed with "<<x.what());
              // try to fix Apple's Mac OS X 10.10 implementation bug
              CRYPTOLOG("Mac OS X 10.10 implementation bug: "
                        "On Mac OSX 10.10 there is a bug in "
                        "PCSC: After a reconnect, first "
                        "transaction (SCardTransmit) fails "
                        "with SCARD_W_RESET_CARD (0x80100068). "
                        "Retry Nr. "<<applebug);
              Certificate c(certificate(keylabel));
              applebug = 0; // successful, reset counter
              return c;
            }
          }
#         endif
        }
        throw no_certfound(keylabel);
      }

      virtual Certificate authenticationCertificate() = 0;
      virtual Certificate digitalSignatureCertificate() = 0;

    protected:
      
      cryptoki::Library _cryptoki;
      
  };

  //! Instance of a Post SuisseID smartcard.
  /*! A SuisseID card issued by Swiss Post.
      @see http://postsuisseid.ch */
  class Post: public Card {
      
    public:

      /// @copydoc Card::Card
      Post(std::shared_ptr<pcsc::Connection::Reader> reader,
           const cryptoki::Library& cryptoki):
          Card(reader, cryptoki), _minPinLen(0), _maxPinLen((unsigned int)-1) {
      }
      
      virtual unsigned int minimalPinLength() {
        if (_minPinLen==0) evaluatePinLengths();
        return _minPinLen;
      }
      
      virtual unsigned int maximalPinLength() {
        if (_maxPinLen==(unsigned int)-1) evaluatePinLengths();
        return _maxPinLen;
      }

      virtual std::string version() {
        if (_version.size()) return _version; // cache the version
        return versionFromMFFile("5649");
      }
      
      virtual CertStatus certStatus() {
        try {
          Certificate auth(authenticationCertificate());
          Certificate sig(digitalSignatureCertificate());
          return VALID;
        } catch (const no_auth& x) {
          return MISSING;
        } catch (const no_digsig& x) {
          return MISSING;
        }
      }
      
      virtual Certificate authenticationCertificate() try {
          return certificate(DIG_SIG);
      } catch (const no_certfound&) {
        throw no_auth();
      }
      
      virtual Certificate digitalSignatureCertificate() try {
        return certificate(NON_REP);
      } catch (const no_certfound&) {
        throw no_digsig();
      }
      
    private:

      void evaluatePinLengths() {
        CRYPTOLOG("log");
        pcsc::Connection::Reader::Transaction lock(_reader);
        selectPkcs15File("4408");
        cardos::BerValues res(readBerFile());
        for (cardos::BerValues::iterator it(res.begin()); it!=res.end(); ++it)
          if ((*it)[0][0].string()=="PIN" ||
              (*it)[0][0].string()=="Digital Signature PIN") {
            if ((*it)[2][0][2].ulong()>_minPinLen)
              _minPinLen = (*it)[2][0][2].ulong();
            if ((*it)[2][0][4].ulong()<_maxPinLen)
              _maxPinLen = (*it)[2][0][4].ulong();
          }
      }

      std::string versionFromMFFile(const std::string& file) {
        CRYPTOLOG("log");
        pcsc::Connection::Reader::Transaction lock(_reader);
        try {
          return _version = cardos::BerValue(readBinary(file))[0].string();
        } catch (const std::exception& x) {
          CRYPTOLOG("exception, no version file: "<<x.what());
          return _version = "<unknown>";
        }
      }

    private:

      std::string _version; // version is cached
      unsigned int _minPinLen; // minimal PIN length is cached
      unsigned int _maxPinLen; // maximal PIN length is cached
  };

  //! List of cards, returned by @ref suisseid::Scanner::scan.
  typedef std::vector<std::shared_ptr<Card> > Cards;
      
  //! Auxiliary SuisseID card manager.
  /** Use this manager to scan your system for SuisseID cards.

      Usage Example:
      
      @code
      #include <suisseid.hxx>
      #include <iostream>

      [...]
      
      try {
        suisseid::Cards cards(suisseid::Scanner().scan());
        for (auto card(cards.begin()); card!=cards.end(); ++card)
          std::cout<<"Found SuisseID: "<<(*card)->name()<<std::endl;
        return 0;
      } catch (std::exception& x) {
        std::cerr<<"**** ERROR in "<<*argv<<": "<<x.what()<<std::endl;
        return 1;
      }
      @endcode */
  class Scanner {
      
    public:

      Scanner(const std::string& lib="libcvP11.so"):
          _cryptoki(lib) {
      }
      
      Scanner(const cryptoki::Library& cryptoki):
          _cryptoki(cryptoki) {
      }

      /// Scan for available known SuisseID cards on the system.
      /** @return List of detected SuisseID smart cards. */
      Cards scan() {
        CRYPTOLOG("log");
        Cards res;
        // By now, scan only for PostSuisseID; in future use factory pattern
        pcsc::Connection::Strings readers
          (pcsc::Connection::getReadersWithAtr("4b53776973735369676e"));
        CRYPTOLOG("number of pcsc-readers: "<<readers.size());
        for (pcsc::Connection::Strings::iterator reader(readers.begin());
             reader!=readers.end(); ++reader) {
          cryptoki::SlotList slots(_cryptoki.slotList(true, *reader));
          CRYPTOLOG("number of cryptoki-readers for "<<*reader
                    <<": "<<slots.size());
          if (slots.size()==1)
            res.push_back(std::shared_ptr<Card>
                          (new Post(pcsc::Connection::reader(*reader),
                                    _cryptoki)));
        }
        return res;
      }
      
    private:
      
      cryptoki::Library _cryptoki;
      
  };

  class StatusCycle {
      
    public:

      StatusCycle(std::shared_ptr<Card> card, unsigned int maxRetries = 20):
          _card(card), _maxRetries(maxRetries), _counter(0) {
      }

      ~StatusCycle() {}

      bool run() {
        CRYPTOLOG("log");
        _counter = 0;
        return start();
      }

    protected:

      std::shared_ptr<Card> card() {
        return _card;
      }

      /// @name                                                           Slots
      //@{

      /// Structure to provide old and new pin
      struct PinPukChange {
          std::string oldpin;
          std::string newpin;
          bool valid() {
            return oldpin.size() && newpin.size();
          }
      };

      /// Pin change required - get pins from user
      virtual PinPukChange pinChange() {
        CRYPTOLOG("log");
        return PinPukChange();
      }
      
      /// Transport pin change required - get pins from user
      virtual PinPukChange pinChangeTransportPin() {
        CRYPTOLOG("log");
        return pinChange();
      }
      
      /// Puk change required - get pins from user
      virtual PinPukChange pinChangePuk() {
        CRYPTOLOG("log");
        return pinChange();
      }

      /// Transport pin locked - you may show an error message
      virtual void transportPinLocked() {
        CRYPTOLOG("log");
      }
      
      /// PKCS#15 pin locked - you may show an error message
      virtual void pkcs15PinLocked() {
        CRYPTOLOG("log");
      }

      /// SigG pin locked - you may show an error message
      virtual void sigGPinLocked() {
        CRYPTOLOG("log");
      }

      /// Puk locked - you may show an error message
      virtual void pukLocked() {
        CRYPTOLOG("log");
      }

      /// Certificates will expire soon - you may show an error message
      virtual void certsExpireSoon() {
        CRYPTOLOG("log");
      }

      /// Certificates are expired soon - you may show an error message
      virtual void certsExpired() {
        CRYPTOLOG("log");
      }

      /// Certificates have been revoked - you may show an error message
      virtual void certsRevoked() {
        CRYPTOLOG("log");
      }

      /// install certificates on the card
      /** parameter @c bool whether to force reinstallation of existing
          certificates
          @return @c true on success */
      virtual bool installCerts(bool = true) {
        CRYPTOLOG("log");
        return false;
      }
      
      //@}

    private:

      bool start() {
        CRYPTOLOG("log");
        if (++_counter>_maxRetries) return false;
        if (_card->transportState())
          return unlockTransportState();
        else
          return checkPkcs15PinStatus();
      }

      bool unlockTransportState() {
        CRYPTOLOG("log");
        if (_card->transportPinRetries()<0)
          return transportPinLocked(), false;
        else
          return changeTransportPin();
      }

      bool changeTransportPin() {
        CRYPTOLOG("log");
        PinPukChange pins(pinChangeTransportPin());
        if (!pins.valid()) return false;
        _card->changePins(pins.newpin, pins.oldpin);
        _card->unsetTransportState();
        return start();
      }

      bool checkPkcs15PinStatus() {
        CRYPTOLOG("log");
        if (_card->pkcs15PinRetries()<0)
          return pkcs15PinLocked(), unlockPkcs15();
        if (_card->pukRetries()<0)
          return pukLocked(), false;
        return checkCertificates();
      }

      bool checkCertificates() {
        CRYPTOLOG("log");
        switch (_card->certStatus()) {
          case Card::MISSING:      return installCerts() && start();
          // case Card::EXPIRES_SOON: certsExpireSoon(); break;
          // case Card::EXPIRED:      return certsExpired(), false;
          // case Card::REVOKED:      return certsRevoked(), false;
          case Card::VALID:        break;
        }
        return checkSigGPinStatus();
      }

      bool checkSigGPinStatus() {
        CRYPTOLOG("log");
        if (_card->sigGPinRetries()<0)
          return sigGPinLocked(), false;
        return true;
      }

      bool unlockPkcs15() {
        CRYPTOLOG("log");
        if (_card->pukRetries()<0)
          return pukLocked(), false;
        PinPukChange pins(pinChangePuk());
        if (!pins.valid()) return false;
        _card->unlock(pins.newpin, pins.oldpin);
        return start();
      }
      
      std::shared_ptr<Card> _card;
      unsigned int _maxRetries;
      unsigned int _counter;
      
  };

  //@}  
}


#endif
