// Filename: makePrcKey.cxx
// Created by:  drose (19Oct04)
//
////////////////////////////////////////////////////////////////////
//
// PANDA 3D SOFTWARE
// Copyright (c) 2001 - 2004, Disney Enterprises, Inc.  All rights reserved
//
// All use of this software is subject to the terms of the Panda 3d
// Software license.  You should have received a copy of this license
// along with this source code; you will also find a current copy of
// the license at http://etc.cmu.edu/panda3d/docs/license/ .
//
// To contact the maintainers of this program write to
// panda3d-general@lists.sourceforge.net .
//
////////////////////////////////////////////////////////////////////

#include "dtoolbase.h"
#include "prcKeyRegistry.h"
#include "filename.h"
#include "pvector.h"
#include <stdio.h>

// Pick up the public key definitions.
#ifdef PRC_PUBLIC_KEYS_INCLUDE
#include PRC_PUBLIC_KEYS_INCLUDE
#endif

#include "openssl/rsa.h"
#include "openssl/err.h"
#include "openssl/pem.h"
#include "openssl/rand.h"
#include "openssl/bio.h"

#ifndef HAVE_GETOPT
  #include "gnu_getopt.h"
#else
  #ifdef HAVE_GETOPT_H
    #include <getopt.h>
  #endif
#endif

class KeyNumber {
public:
  int _number;
  bool _got_pass_phrase;
  string _pass_phrase;
};
typedef pvector<KeyNumber> KeyNumbers;

////////////////////////////////////////////////////////////////////
//     Function: output_ssl_errors
//  Description: A convenience function that is itself a wrapper
//               around the OpenSSL convenience function to output the
//               recent OpenSSL errors.  This function sends the error
//               string to cerr.
////////////////////////////////////////////////////////////////////
void
output_ssl_errors() {
  cerr << "Error occurred in SSL routines.\n";

  static bool strings_loaded = false;
  if (!strings_loaded) {
    ERR_load_crypto_strings();
    strings_loaded = true;
  }

  unsigned long e = ERR_get_error();
  while (e != 0) {
    static const size_t buffer_len = 256;
    char buffer[buffer_len];
    ERR_error_string_n(e, buffer, buffer_len);
    cerr << buffer << "\n";
    e = ERR_get_error();
  }
}

////////////////////////////////////////////////////////////////////
//     Function: output_c_string
//  Description: Extracts the data written to the indicated memory bio
//               and writes it to the indicated stream, formatting it
//               to be compiled into a C or C++ program as a string.
////////////////////////////////////////////////////////////////////
void
output_c_string(ostream &out, const string &string_name, 
                int index, BIO *mbio) {
  char *data_ptr;
  size_t data_size = BIO_get_mem_data(mbio, &data_ptr);

  out << "static const char * const " << string_name
      << index << "_data =\n"
      << "  \"";

  bool last_nl = false;
  for (size_t i = 0; i < data_size; i++) {
    if (data_ptr[i] == '\n') {
      out << "\\n";
      last_nl = true;

    } else {
      if (last_nl) {
        out << "\"\n  \"";
        last_nl = false;
      }

      if (isprint(data_ptr[i])) {
        out << data_ptr[i];

      } else {
        out << "\\x" << hex << setw(2) << setfill('0') 
            << (unsigned int)(unsigned char)data_ptr[i] << dec;
      }
    }
  }
  out << "\";\nstatic const unsigned int " << string_name << index
      << "_length = " << data_size << ";\n";
}

////////////////////////////////////////////////////////////////////
//     Function: generate_key
//  Description: Generates a new public and private key pair.
////////////////////////////////////////////////////////////////////
EVP_PKEY *
generate_key() {
  RSA *rsa = RSA_generate_key(1024, 7, NULL, NULL);
  
  if (rsa == (RSA *)NULL) {
    output_ssl_errors();
    exit(1);
  }

  EVP_PKEY *pkey = EVP_PKEY_new();
  EVP_PKEY_assign_RSA(pkey, rsa);

  return pkey;
}

////////////////////////////////////////////////////////////////////
//     Function: write_public_keys
//  Description: Writes the list of public keys stored in the
//               PrcKeyRegistry to the indicated output filename as a
//               compilable list of KeyDef entries, suitable for
//               passing to PrcKeyRegistry::record_keys().
////////////////////////////////////////////////////////////////////
void
write_public_keys(Filename outfile) {
  outfile.set_text();
  cerr << "Rewriting " << outfile << "\n";

  ofstream out;
  if (!outfile.open_write(out)) {
    cerr << "Unable to open " << outfile << " for writing.\n";
    exit(1);
  }

  out <<
    "\n"
    "// This file was generated by make-prc-key.  It defines the public keys\n"
    "// that will be used to validate signed prc files.\n"
    "\n"
    "#include \"prcKeyRegistry.h\"\n"
    "\n";
  
  PrcKeyRegistry *pkr = PrcKeyRegistry::get_global_ptr();

  BIO *mbio = BIO_new(BIO_s_mem());

  int num_keys = pkr->get_num_keys();
  int i;
  for (i = 0; i < num_keys; i++) {
    EVP_PKEY *pkey = pkr->get_key(i);

    if (pkey != (EVP_PKEY *)NULL) {
      if (!PEM_write_bio_PUBKEY(mbio, pkey)) {
        output_ssl_errors();
        exit(1);
      }

      output_c_string(out, "prc_pubkey", i, mbio);
      BIO_reset(mbio);
      out << "\n";
    }
  }

  BIO_free(mbio);

  // Now output the table that indexes all of the above.
  out << "static PrcKeyRegistry::KeyDef const prc_pubkeys[" << num_keys << "] = {\n";

  for (i = 0; i < num_keys; i++) {
    EVP_PKEY *pkey = pkr->get_key(i);
    time_t generated_time = pkr->get_generated_time(i);

    if (pkey != (EVP_PKEY *)NULL) {
      out << "  { prc_pubkey" << i << "_data, prc_pubkey" << i 
          << "_length, " << generated_time << " },\n";
    } else {
      out << "  { NULL, 0, 0 },\n";
    }
  };

  out << "};\n"
      << "static const int num_prc_pubkeys = " << num_keys << ";\n\n";
}

////////////////////////////////////////////////////////////////////
//     Function: write_private_key
//  Description: Generates a C++ program that can be used to sign a
//               prc file with the indicated private key into the
//               given output filename.
////////////////////////////////////////////////////////////////////
void
write_private_key(EVP_PKEY *pkey, Filename outfile, int n, time_t now,
                  const char *pp) {
  outfile.set_text();
  cerr << "Rewriting " << outfile << "\n";

  ofstream out;
  if (!outfile.open_write(out)) {
    cerr << "Unable to open " << outfile << " for writing.\n";
    exit(1);
  }

  out <<
    "\n"
    "// This file was generated by make-prc-key.  It can be compiled against\n"
    "// dtool to produce a program that will sign a prc file using key number " << n << ".\n\n";

  BIO *mbio = BIO_new(BIO_s_mem());

  int write_result;
  if (pp != NULL && *pp == '\0') {
    // The supplied password was the empty string.  This means not to
    // encrypt the private key.
    write_result =
      PEM_write_bio_PKCS8PrivateKey(mbio, pkey, NULL, NULL, 0, NULL, NULL);

  } else {
    // Otherwise, the default is to encrypt it.
    write_result =
      PEM_write_bio_PKCS8PrivateKey(mbio, pkey, EVP_des_ede3_cbc(),
                                    NULL, 0, NULL, (void *)pp);
  }

  if (!write_result) {
    output_ssl_errors();
    exit(1);
  }

  output_c_string(out, "prc_privkey", n, mbio);

  BIO_free(mbio);

  out << 
    "\n\n"
    "#define KEY_NUMBER " << n << "\n"
    "#define KEY_DATA prc_privkey" << n << "_data\n"
    "#define KEY_LENGTH prc_privkey" << n << "_length\n"
    "#define PROGNAME \"" << outfile.get_basename_wo_extension() << "\"\n"
    "#define GENERATED_TIME " << now << "\n\n"

    "#include \"signPrcFile_src.cxx\"\n\n";
}

////////////////////////////////////////////////////////////////////
//     Function: usage
//  Description: 
////////////////////////////////////////////////////////////////////
void
usage() {
  cerr <<
    "\nmake-prc-key [opts] 1[,\"pass_phrase\"] [2[,\"pass phrase\"] 3 ...]\n\n"

    "This program generates one or more new keys to be used for signing\n"
    "a prc file.  The key itself is a completely arbitrary random bit\n"
    "sequence.  It is divided into a public and a private key; the public\n"
    "key is not secret and will be compiled into libdtool, while the private\n"
    "key should be safeguarded and will be written into a .cxx file that\n"
    "can be compiled as a standalone application.\n\n"
    
    "The output is a public and private key pair for each trust level.  The\n"
    "form of the output for both public and private keys will be compilable\n"
    "C++ code; see -a and -b, below, for a complete description.\n\n"
    
    "After the options, the remaining arguments list the individual trust\n"
    "level keys to generate.  For each integer specified, a different key\n"
    "will be created.  There should be one key for each trust level\n"
    "required; a typical application will only need one or two keys.\n\n"
  
    "Options:\n\n"
    
    "   -a pub_outfile.cxx\n"
    "       Specifies the name and location of the public key output file\n"
    "       to generate.  This file must then be named by the Config.pp\n"
    "       variable PRC_PUBLIC_KEYS_FILENAME so that it will be compiled\n"
    "       in with libdtool and available to verify signatures.  If this\n"
    "       option is omitted, the previously-compiled value is used.\n\n"

    "   -b priv_outfile#.cxx\n"
    "       Specifies the name and location of the private key output file(s)\n"
    "       to generate.  A different output file will be generated for each\n"
    "       different trust level; the hash mark '#' appearing in the file\n"
    "       name will be filled in with the corresponding numeric trust level.\n"
    "       The hash mark may be omitted if you only require one trust level.\n"
    "       When compiled against dtool, each of these files will generate\n"
    "       a program that can be used to sign a prc file with the corresponding\n"
    "       trust level.\n\n"

    "   -p \"[pass phrase]\"\n"
    "       Uses the indicated pass phrase to encrypt the private key.\n"
    "       This specifies an overall pass phrase; you may also specify\n"
    "       a different pass phrase for each key by using the key,\"pass phrase\"\n"
    "       syntax.\n\n"

    "       If a pass phrase is not specified on the command line, you will be\n"
    "       prompted interactively.  Every user of the signing programs\n"
    "       (outfile_sign1.cxx, etc.) will need to know the pass phrase\n"
    "       in order to sign prc files.\n\n"

    "       If this is specified as the empty string (\"\"), then the key\n"
    "       will not be encrypted, and anyone can run the signing\n"
    "       programs without having to supply a pass phrase.\n\n";
}

////////////////////////////////////////////////////////////////////
//     Function: main
//  Description: 
////////////////////////////////////////////////////////////////////
int
main(int argc, char *argv[]) {
  extern char *optarg;
  extern int optind;
  const char *optstr = "a:b:p:h";

  Filename pub_outfile;
  bool got_pub_outfile = false;
  Filename priv_outfile;
  bool got_priv_outfile = false;
  string pass_phrase;
  bool got_pass_phrase = false;

  int flag = getopt(argc, argv, optstr);

  while (flag != EOF) {
    switch (flag) {
    case 'a':
      pub_outfile = optarg;
      got_pub_outfile = true;
      break;

    case 'b':
      priv_outfile = optarg;
      got_priv_outfile = true;
      break;

    case 'p':
      pass_phrase = optarg;
      got_pass_phrase = true;
      break;

    case 'h':
      usage();
      exit(0);

    default:
      exit(1);
    }
    flag = getopt(argc, argv, optstr);
  }

  argc -= (optind-1);
  argv += (optind-1);

  if (argc < 2) {
    usage();
    exit(1);
  }

  if (got_pub_outfile) {
    if (pub_outfile.get_extension() != "cxx") {
      cerr << "Public key output file '" << pub_outfile
           << "' should have a .cxx extension.\n";
      exit(1);
    }
  } else {
#ifdef PRC_PUBLIC_KEYS_INCLUDE
    PrcKeyRegistry::get_global_ptr()->record_keys(prc_pubkeys, num_prc_pubkeys);
    pub_outfile = PRC_PUBLIC_KEYS_FILENAME;
#endif

    if (pub_outfile.empty()) {
      cerr << "No -a specified, and no PRC_PUBLIC_KEYS_FILENAME variable\n"
           << "compiled in.\n\n";
      exit(1);
    }
  }

  if (got_priv_outfile) {
    if (priv_outfile.get_extension() != "cxx") {
      cerr << "Private key output file '" << priv_outfile
           << "' should have a .cxx extension.\n";
      exit(1);
    }

  } else {
    cerr << "You must use the -b option to specify the private key output filenames.\n";
    exit(1);
  }

  KeyNumbers key_numbers;
  for (int i = 1; i < argc; i++) {
    KeyNumber key;
    char *endptr;
    key._number = strtol(argv[i], &endptr, 0);
    key._got_pass_phrase = got_pass_phrase;
    key._pass_phrase = pass_phrase;

    if (*endptr == ',') {
      // Here's a pass phrase for this particular key.
      key._got_pass_phrase = true;
      key._pass_phrase = endptr + 1;
    } else if (*endptr) {
      cerr << "Parameter '" << argv[i] << "' should be an integer.\n";
      exit(1);
    }
    if (key._number <= 0) {
      cerr << "Key numbers must be greater than 0; you specified " 
           << key._number << ".\n";
      exit(1);
    }
    key_numbers.push_back(key);
  }

  // Seed the random number generator.
  RAND_status();

  // Load the OpenSSL algorithms.
  OpenSSL_add_all_algorithms();

  time_t now = time(NULL);

  string name = priv_outfile.get_fullpath_wo_extension();
  string prefix, suffix;
  bool got_hash;

  size_t hash = name.find('#');
  if (hash == string::npos) {
    prefix = name;
    suffix = ".cxx";
    got_hash = false;

  } else {
    prefix = name.substr(0, hash);
    suffix = name.substr(hash + 1) + ".cxx";
    got_hash = true;
  }

  KeyNumbers::iterator ki;
  for (ki = key_numbers.begin(); ki != key_numbers.end(); ++ki) {
    int n = (*ki)._number;
    const char *pp = NULL;
    if ((*ki)._got_pass_phrase) {
      pp = (*ki)._pass_phrase.c_str();
    }

    EVP_PKEY *pkey = generate_key();
    PrcKeyRegistry::get_global_ptr()->set_key(n, pkey, now);

    ostringstream strm;
    if (got_hash || n != 1) {
      // If we got an explicit hash mark, we always output the number.
      // If we did not get an explicit hash mark, we output the number
      // only if it is other than 1.
      strm << prefix << n << suffix;

    } else {
      // If we did not get an explicit hash mark in the filename, we
      // omit the number for key 1 (this might be the only key, and
      // so maybe the user doesn't require a number designator).
      strm << prefix << suffix;
    }

    write_private_key(pkey, strm.str(), n, now, pp);
  }

  write_public_keys(pub_outfile);

  return (0);
}
