/*
 * Copyright (C) 1997-2003 Thomas Roessler <roessler@does-not-exist.org>
 *
 *     This program is free software; you can redistribute it
 *     and/or modify it under the terms of the GNU General Public
 *     License as published by the Free Software Foundation; either
 *     version 2 of the License, or (at your option) any later
 *     version.
 *
 *     This program is distributed in the hope that it will be
 *     useful, but WITHOUT ANY WARRANTY; without even the implied
 *     warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
 *     PURPOSE.  See the GNU General Public License for more
 *     details.
 *
 *     You should have received a copy of the GNU General Public
 *     License along with this program; if not, write to the Free
 *     Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 *     Boston, MA  02110-1301, USA.
 */

/*
 * This is a "simple" PGP key ring dumper.
 *
 * The output format is supposed to be compatible to the one GnuPG
 * emits and Mutt expects.
 *
 * Note that the code of this program could be considerably less
 * complex, but most of it was taken from mutt's second generation
 * key ring parser.
 *
 * You can actually use this to put together some fairly general
 * PGP key management applications.
 *
 */

#if HAVE_CONFIG_H
# include "config.h"
#endif

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#ifdef HAVE_GETOPT_H
# include <getopt.h>
#else
extern char *optarg;
extern int optind;
#endif

#include "sha1.h"
#include "md5.h"
#include "lib.h"
#include "pgplib.h"
#include "pgppacket.h"

#define MD5_DIGEST_LENGTH  16

#ifdef HAVE_FGETPOS
#define FGETPOS(fp,pos) fgetpos((fp),&(pos))
#define FSETPOS(fp,pos) fsetpos((fp),&(pos))
#else
#define FGETPOS(fp,pos) pos=ftello((fp));
#define FSETPOS(fp,pos) fseeko((fp),(pos),SEEK_SET)
#endif


static short dump_signatures = 0;
static short dump_fingerprints = 0;


static void pgpring_find_candidates(char *ringfile, const char *hints[], int nhints);
static void pgpring_dump_keyblock(pgp_key_t p);

int main(int argc, char * const argv[])
{
  int c;

  short version = 2;
  short secring = 0;

  const char *_kring = NULL;
  char *env_pgppath, *env_home;

  char pgppath[_POSIX_PATH_MAX];
  char kring[_POSIX_PATH_MAX+12];

  while ((c = getopt(argc, argv, "f25sk:S")) != EOF)
  {
    switch (c)
    {
      case 'S':
      {
        dump_signatures = 1;
        break;
      }

      case 'f':
      {
        dump_fingerprints = 1;
        break;
      }

      case 'k':
      {
        _kring = optarg;
        break;
      }

      case '2': case '5':
      {
        version = c - '0';
        break;
      }

      case 's':
      {
        secring = 1;
        break;
      }

      default:
      {
        fprintf(stderr, "usage: %s [-k <key ring> | [-2 | -5] [ -s] [-S] [-f]] [hints]\n",
                argv[0]);
        exit(1);
      }
    }
  }

  if (_kring)
    strfcpy(kring, _kring, sizeof(kring));
  else
  {
    if ((env_pgppath = getenv("PGPPATH")))
      strfcpy(pgppath, env_pgppath, sizeof(pgppath));
    else if ((env_home = getenv("HOME")))
      snprintf(pgppath, sizeof(pgppath), "%s/.pgp", env_home);
    else
    {
      fprintf(stderr, "%s: Can't determine your PGPPATH.\n",
              argc ? argv[0] : "mutt_pgpring");
      exit(1);
    }

    if (secring)
      snprintf(kring, sizeof(kring), "%s/secring.%s", pgppath, version == 2 ? "pgp" : "skr");
    else
      snprintf(kring, sizeof(kring), "%s/pubring.%s", pgppath, version == 2 ? "pgp" : "pkr");
  }


  if (optind > argc)
    optind = argc;

  pgpring_find_candidates(kring, (const char**) argv + optind, argc - optind);

  return 0;
}

static char *binary_fingerprint_to_string(unsigned char *buff, size_t length)
{
  int i;
  char *fingerprint, *pf;

  pf = fingerprint = (char *)safe_malloc((length * 2) + 1);

  for (i = 0; i < length; i++)
  {
    sprintf(pf, "%02X", buff[i]);
    pf += 2;
  }
  *pf = 0;

  return fingerprint;
}


/* The actual key ring parser */
static void pgp_make_pgp2_fingerprint(unsigned char *buff,
                                      unsigned char *digest)
{
  struct md5_ctx ctx;
  unsigned int size = 0;

  md5_init_ctx(&ctx);

  size = (buff[0] << 8) + buff[1];
  size = ((size + 7) / 8);
  buff = &buff[2];

  md5_process_bytes(buff, size, &ctx);

  buff = &buff[size];

  size = (buff[0] << 8) + buff[1];
  size = ((size + 7) / 8);
  buff = &buff[2];

  md5_process_bytes(buff, size, &ctx);

  md5_finish_ctx(&ctx, digest);
} /* pgp_make_pgp2_fingerprint() */

static pgp_key_t pgp_parse_pgp2_key(unsigned char *buff, size_t l)
{
  pgp_key_t p;
  unsigned char alg;
  unsigned char digest[MD5_DIGEST_LENGTH];
  size_t expl;
  unsigned long id;
  time_t gen_time = 0;
  unsigned short exp_days = 0;
  size_t j;
  int i, k;
  unsigned char scratch[LONG_STRING];

  if (l < 12)
    return NULL;

  p = pgp_new_keyinfo();

  for (i = 0, j = 2; i < 4; i++)
    gen_time = (gen_time << 8) + buff[j++];

  p->gen_time = gen_time;

  for (i = 0; i < 2; i++)
    exp_days = (exp_days << 8) + buff[j++];

  if (exp_days && time(NULL) > gen_time + exp_days * 24 * 3600)
    p->flags |= KEYFLAG_EXPIRED;

  alg = buff[j++];

  p->numalg = alg;
  p->algorithm = pgp_pkalgbytype(alg);
  p->flags |= pgp_get_abilities(alg);

  if (dump_fingerprints)
  {
    /* j now points to the key material, which we need for the fingerprint */
    pgp_make_pgp2_fingerprint(&buff[j], digest);
    p->fingerprint = binary_fingerprint_to_string(digest, MD5_DIGEST_LENGTH);
  }

  expl = 0;
  for (i = 0; i < 2; i++)
    expl = (expl << 8) + buff[j++];

  p->keylen = expl;

  expl = (expl + 7) / 8;
  if (expl < 4)
    goto bailout;


  j += expl - 8;

  for (k = 0; k < 2; k++)
  {
    for (id = 0, i = 0; i < 4; i++)
      id = (id << 8) + buff[j++];

    snprintf((char *) scratch + k * 8, sizeof(scratch) - k * 8,
             "%08lX", id);
  }

  p->keyid = safe_strdup((char *) scratch);

  return p;

bailout:

  FREE(&p);
  return NULL;
}

static void pgp_make_pgp3_fingerprint(unsigned char *buff, size_t l,
                                      unsigned char *digest)
{
  unsigned char dummy;
  SHA1_CTX context;

  SHA1_Init(&context);

  dummy = buff[0] & 0x3f;

  if (dummy == PT_SUBSECKEY || dummy == PT_SUBKEY || dummy == PT_SECKEY)
    dummy = PT_PUBKEY;

  dummy = (dummy << 2) | 0x81;
  SHA1_Update(&context, &dummy, 1);
  dummy = ((l - 1) >> 8) & 0xff;
  SHA1_Update(&context, &dummy, 1);
  dummy = (l - 1) & 0xff;
  SHA1_Update(&context, &dummy, 1);
  SHA1_Update(&context, buff + 1, l - 1);
  SHA1_Final(digest, &context);

}

static void skip_bignum(unsigned char *buff, size_t l, size_t j,
                        size_t * toff, size_t n)
{
  size_t len;

  do
  {
    len = (buff[j] << 8) + buff[j + 1];
    j += (len + 7) / 8 + 2;
  }
  while (j <= l && --n > 0);

  if (toff)
    *toff = j;
}


static pgp_key_t pgp_parse_pgp3_key(unsigned char *buff, size_t l)
{
  pgp_key_t p;
  unsigned char alg;
  unsigned char digest[SHA_DIGEST_LENGTH];
  unsigned char scratch[LONG_STRING];
  time_t gen_time = 0;
  unsigned long id;
  int i, k;
  short len;
  size_t j;

  p = pgp_new_keyinfo();
  j = 2;

  for (i = 0; i < 4; i++)
    gen_time = (gen_time << 8) + buff[j++];

  p->gen_time = gen_time;

  alg = buff[j++];

  p->numalg = alg;
  p->algorithm = pgp_pkalgbytype(alg);
  p->flags |= pgp_get_abilities(alg);

  len = (buff[j] << 8) + buff[j + 1];
  p->keylen = len;

  if (alg >= 1 && alg <= 3)
    skip_bignum(buff, l, j, &j, 2);
  else if (alg == 16 || alg == 20)
    skip_bignum(buff, l, j, &j, 3);
  else if (alg == 17)
    skip_bignum(buff, l, j, &j, 4);

  pgp_make_pgp3_fingerprint(buff, j, digest);
  if (dump_fingerprints)
  {
    p->fingerprint = binary_fingerprint_to_string(digest, SHA_DIGEST_LENGTH);
  }

  for (k = 0; k < 2; k++)
  {
    for (id = 0, i = SHA_DIGEST_LENGTH - 8 + k * 4;
         i < SHA_DIGEST_LENGTH + (k - 1) * 4; i++)
      id = (id << 8) + digest[i];

    snprintf((char *) scratch + k * 8, sizeof(scratch) - k * 8, "%08lX", id);
  }

  p->keyid = safe_strdup((char *) scratch);

  return p;
}

static pgp_key_t pgp_parse_keyinfo(unsigned char *buff, size_t l)
{
  if (!buff || l < 2)
    return NULL;

  switch (buff[1])
  {
    case 2:
    case 3:
      return pgp_parse_pgp2_key(buff, l);
    case 4:
      return pgp_parse_pgp3_key(buff, l);
    default:
      return NULL;
  }
}

static int pgp_parse_pgp2_sig(unsigned char *buff, size_t l,
                              pgp_key_t p, pgp_sig_t *s)
{
  unsigned char sigtype;
  time_t sig_gen_time;
  unsigned long signerid1;
  unsigned long signerid2;
  size_t j;
  int i;

  if (l < 22)
    return -1;

  j = 3;
  sigtype = buff[j++];

  sig_gen_time = 0;
  for (i = 0; i < 4; i++)
    sig_gen_time = (sig_gen_time << 8) + buff[j++];

  signerid1 = signerid2 = 0;
  for (i = 0; i < 4; i++)
    signerid1 = (signerid1 << 8) + buff[j++];

  for (i = 0; i < 4; i++)
    signerid2 = (signerid2 << 8) + buff[j++];


  if (sigtype == 0x20 || sigtype == 0x28)
    p->flags |= KEYFLAG_REVOKED;

  if (s)
  {
    s->sigtype = sigtype;
    s->sid1    = signerid1;
    s->sid2    = signerid2;
  }

  return 0;
}

static int pgp_parse_pgp3_sig(unsigned char *buff, size_t l,
                              pgp_key_t p, pgp_sig_t *s)
{
  unsigned char sigtype;
  unsigned char skt;
  time_t sig_gen_time = -1;
  long validity = -1;
  long key_validity = -1;
  unsigned long signerid1 = 0;
  unsigned long signerid2 = 0;
  size_t ml;
  size_t j;
  int i;
  short ii;
  short have_critical_spks = 0;

  if (l < 7)
    return -1;

  j = 2;

  sigtype = buff[j++];
  j += 2; /* pkalg, hashalg */

  for (ii = 0; ii < 2; ii++)
  {
    size_t skl;
    size_t nextone;

    ml = (buff[j] << 8) + buff[j + 1];
    j += 2;

    if (j + ml > l)
      break;

    nextone = j;
    while (ml)
    {
      j = nextone;
      skl = buff[j++];
      if (!--ml)
        break;

      if (skl >= 192)
      {
        skl = (skl - 192) * 256 + buff[j++] + 192;
        if (!--ml)
          break;
      }

      if ((int) ml - (int) skl < 0)
        break;
      ml -= skl;

      nextone = j + skl;
      skt = buff[j++];

      switch (skt & 0x7f)
      {
        case 2:                 /* creation time */
        {
          if (skl < 4)
            break;
          sig_gen_time = 0;
          for (i = 0; i < 4; i++)
            sig_gen_time = (sig_gen_time << 8) + buff[j++];

          break;
        }
        case 3:                 /* expiration time */
        {
          if (skl < 4)
            break;
          validity = 0;
          for (i = 0; i < 4; i++)
            validity = (validity << 8) + buff[j++];
          break;
        }
        case 9:                 /* key expiration time */
        {
          if (skl < 4)
            break;
          key_validity = 0;
          for (i = 0; i < 4; i++)
            key_validity = (key_validity << 8) + buff[j++];
          break;
        }
        case 16:                        /* issuer key ID */
        {
          if (skl < 8)
            break;
          signerid2 = signerid1 = 0;
          for (i = 0; i < 4; i++)
            signerid1 = (signerid1 << 8) + buff[j++];
          for (i = 0; i < 4; i++)
            signerid2 = (signerid2 << 8) + buff[j++];

          break;
        }
        case 10:                        /* CMR key */
          break;
        case 4:                         /* exportable */
        case 5:                         /* trust */
        case 6:                         /* regexp */
        case 7:                         /* revocable */
        case 11:                        /* Pref. symm. alg. */
        case 12:                        /* revocation key */
        case 20:                        /* notation data */
        case 21:                        /* pref. hash */
        case 22:                        /* pref. comp.alg. */
        case 23:                        /* key server prefs. */
        case 24:                        /* pref. key server */
        default:
        {
          if (skt & 0x80)
            have_critical_spks = 1;
        }
      }
    }
    j = nextone;
  }

  if (sigtype == 0x20 || sigtype == 0x28)
    p->flags |= KEYFLAG_REVOKED;
  if (key_validity != -1 && time(NULL) > p->gen_time + key_validity)
    p->flags |= KEYFLAG_EXPIRED;
  if (have_critical_spks)
    p->flags |= KEYFLAG_CRITICAL;

  if (s)
  {
    s->sigtype = sigtype;
    s->sid1    = signerid1;
    s->sid2    = signerid2;
  }


  return 0;

}


static int pgp_parse_sig(unsigned char *buff, size_t l,
                         pgp_key_t p, pgp_sig_t *sig)
{
  if (!buff || l < 2 || !p)
    return -1;

  switch (buff[1])
  {
    case 2:
    case 3:
      return pgp_parse_pgp2_sig(buff, l, p, sig);
    case 4:
      return pgp_parse_pgp3_sig(buff, l, p, sig);
    default:
      return -1;
  }
}

/* parse one key block, including all subkeys. */

static pgp_key_t pgp_parse_keyblock(FILE * fp)
{
  unsigned char *buff;
  unsigned char pt = 0;
  unsigned char last_pt;
  size_t l;
  short err = 0;

#ifdef HAVE_FGETPOS
  fpos_t pos;
#else
  LOFF_T pos;
#endif

  pgp_key_t root = NULL;
  pgp_key_t *last = &root;
  pgp_key_t p = NULL;
  pgp_uid_t *uid = NULL;
  pgp_uid_t **addr = NULL;
  pgp_sig_t **lsig = NULL;

  FGETPOS(fp,pos);

  while (!err && (buff = pgp_read_packet(fp, &l)) != NULL)
  {
    last_pt = pt;
    pt = buff[0] & 0x3f;

    /* check if we have read the complete key block. */

    if ((pt == PT_SECKEY || pt == PT_PUBKEY) && root)
    {
      FSETPOS(fp, pos);
      return root;
    }

    switch (pt)
    {
      case PT_SECKEY:
      case PT_PUBKEY:
      case PT_SUBKEY:
      case PT_SUBSECKEY:
      {
        if (!(*last = p = pgp_parse_keyinfo(buff, l)))
        {
          err = 1;
          break;
        }

        last = &p->next;
        addr = &p->address;
        lsig = &p->sigs;

        if (pt == PT_SUBKEY || pt == PT_SUBSECKEY)
        {
          p->flags |= KEYFLAG_SUBKEY;
          if (p != root)
          {
            p->parent  = root;
            p->address = pgp_copy_uids(root->address, p);
            while (*addr) addr = &(*addr)->next;
          }
        }

        if (pt == PT_SECKEY || pt == PT_SUBSECKEY)
          p->flags |= KEYFLAG_SECRET;

        break;
      }

      case PT_SIG:
      {
        if (lsig)
        {
          pgp_sig_t *signature = safe_calloc(sizeof(pgp_sig_t), 1);
          *lsig = signature;
          lsig = &signature->next;

          pgp_parse_sig(buff, l, p, signature);
        }
        break;
      }

      case PT_TRUST:
      {
        if (p && (last_pt == PT_SECKEY || last_pt == PT_PUBKEY ||
                  last_pt == PT_SUBKEY || last_pt == PT_SUBSECKEY))
        {
          if (buff[1] & 0x20)
          {
            p->flags |= KEYFLAG_DISABLED;
          }
        }
        else if (last_pt == PT_NAME && uid)
        {
          uid->trust = buff[1];
        }
        break;
      }
      case PT_NAME:
      {
        char *chr;


        if (!addr)
          break;

        chr = safe_malloc(l);
        memcpy(chr, buff + 1, l - 1);
        chr[l - 1] = '\0';


        *addr = uid = safe_calloc(1, sizeof(pgp_uid_t)); /* XXX */
        uid->addr = chr;
        uid->parent = p;
        uid->trust = 0;
        addr = &uid->next;
        lsig = &uid->sigs;

        /* the following tags are generated by
         * pgp 2.6.3in.
         */

        if (strstr(chr, "ENCR"))
          p->flags |= KEYFLAG_PREFER_ENCRYPTION;
        if (strstr(chr, "SIGN"))
          p->flags |= KEYFLAG_PREFER_SIGNING;

        break;
      }
    }

    FGETPOS(fp,pos);
  }

  if (err)
    pgp_free_key(&root);

  return root;
}

static int pgpring_string_matches_hint(const char *s, const char *hints[], int nhints)
{
  int i;

  if (!hints || !nhints)
    return 1;

  for (i = 0; i < nhints; i++)
  {
    if (mutt_stristr(s, hints[i]) != NULL)
      return 1;
  }

  return 0;
}

/*
 * Go through the key ring file and look for keys with
 * matching IDs.
 */

static void pgpring_find_candidates(char *ringfile, const char *hints[], int nhints)
{
  FILE *rfp;
#ifdef HAVE_FGETPOS
  fpos_t pos, keypos;
#else
  LOFF_T pos, keypos;
#endif

  unsigned char *buff = NULL;
  unsigned char pt = 0;
  size_t l = 0;

  short err = 0;

  if ((rfp = fopen(ringfile, "r")) == NULL)
  {
    char *error_buf;
    size_t error_buf_len;

    error_buf_len = sizeof("fopen: ") - 1 + strlen(ringfile) + 1;
    error_buf = safe_malloc(error_buf_len);
    snprintf(error_buf, error_buf_len, "fopen: %s", ringfile);
    perror(error_buf);
    FREE(&error_buf);
    return;
  }

  FGETPOS(rfp,pos);
  FGETPOS(rfp,keypos);

  while (!err && (buff = pgp_read_packet(rfp, &l)) != NULL)
  {
    pt = buff[0] & 0x3f;

    if (l < 1)
      continue;

    if ((pt == PT_SECKEY) || (pt == PT_PUBKEY))
    {
      keypos = pos;
    }
    else if (pt == PT_NAME)
    {
      char *tmp = safe_malloc(l);

      memcpy(tmp, buff + 1, l - 1);
      tmp[l - 1] = '\0';

      /* mutt_decode_utf8_string (tmp, chs); */

      if (pgpring_string_matches_hint(tmp, hints, nhints))
      {
        pgp_key_t p;

        FSETPOS(rfp, keypos);

        /* Not bailing out here would lead us into an endless loop. */

        if ((p = pgp_parse_keyblock(rfp)) == NULL)
          err = 1;

        pgpring_dump_keyblock(p);
        pgp_free_key(&p);
      }

      FREE(&tmp);
    }

    FGETPOS(rfp,pos);
  }

  safe_fclose(&rfp);

}

static void print_userid(const char *id)
{
  for (; id && *id; id++)
  {
    if (*id >= ' ' && *id <= 'z' && *id != ':')
      putchar(*id);
    else
      printf("\\x%02x", (*id) & 0xff);
  }
}

static void print_fingerprint(pgp_key_t p)
{
  if (!p->fingerprint)
    return;

  printf("fpr:::::::::%s:\n", p->fingerprint);
} /* print_fingerprint() */


static void pgpring_dump_signatures(pgp_sig_t *sig)
{
  for (; sig; sig = sig->next)
  {
    if (sig->sigtype == 0x10 || sig->sigtype == 0x11 ||
        sig->sigtype == 0x12 || sig->sigtype == 0x13)
      printf("sig::::%08lX%08lX::::::%X:\n",
             sig->sid1, sig->sid2, sig->sigtype);
    else if (sig->sigtype == 0x20)
      printf("rev::::%08lX%08lX::::::%X:\n",
             sig->sid1, sig->sid2, sig->sigtype);
  }
}


static char gnupg_trustletter(int t)
{
  switch (t)
  {
    case 1: return 'n';
    case 2: return 'm';
    case 3: return 'f';
  }
  return 'q';
}

static void pgpring_dump_keyblock(pgp_key_t p)
{
  pgp_uid_t *uid;
  short first;
  struct tm *tp;
  time_t t;

  for (; p; p = p->next)
  {
    first = 1;

    if (p->flags & KEYFLAG_SECRET)
    {
      if (p->flags & KEYFLAG_SUBKEY)
        printf("ssb:");
      else
        printf("sec:");
    }
    else
    {
      if (p->flags & KEYFLAG_SUBKEY)
        printf("sub:");
      else
        printf("pub:");
    }

    if (p->flags & KEYFLAG_REVOKED)
      putchar('r');
    if (p->flags & KEYFLAG_EXPIRED)
      putchar('e');
    if (p->flags & KEYFLAG_DISABLED)
      putchar('d');

    for (uid = p->address; uid; uid = uid->next, first = 0)
    {
      if (!first)
      {
        printf("uid:%c::::::::", gnupg_trustletter(uid->trust));
        print_userid(uid->addr);
        printf(":\n");
      }
      else
      {
        if (p->flags & KEYFLAG_SECRET)
          putchar('u');
        else
          putchar(gnupg_trustletter(uid->trust));

        t = p->gen_time;
        tp = gmtime(&t);

        printf(":%d:%d:%s:%04d-%02d-%02d::::", p->keylen, p->numalg, p->keyid,
               1900 + tp->tm_year, tp->tm_mon + 1, tp->tm_mday);

        print_userid(uid->addr);
        printf("::");

        if (pgp_canencrypt(p->numalg))
          putchar('e');
        if (pgp_cansign(p->numalg))
          putchar('s');
        if (p->flags & KEYFLAG_DISABLED)
          putchar('D');
        printf(":\n");

        if (dump_fingerprints)
          print_fingerprint(p);
      }

      if (dump_signatures)
      {
        if (first) pgpring_dump_signatures(p->sigs);
        pgpring_dump_signatures(uid->sigs);
      }
    }
  }
}
