#!/usr/bin/perl

# vim:expandtab:ai:tabstop=2:textwidth=78:softtabstop=2:shiftwidth=2

use strict;
use warnings;
use Getopt::Long;

#
# Copyright (c) 2008 - present Phil Dibowitz (phil@ipom.com)
#
#   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, version 2.
#
# Given a PGP Keyring, generate a worksheet for a keysigning party.
#
# This is largely based on party-table.pl by
#   V. Alex Brennen <vab@cryptnet.net> and Gerfried Fuchs <alfie@ist.org>
# You can find the original at http://www.cryptnet.net/people/vab/
#

use constant VERSION => '2.2.6';

sub print_html_header
{
  my $extra_fields = shift;
  print "<html>\n<head><title>PGP Keysigning Party Keys</title></head>\n";
  print "<body>\n<table border=1>\n";
  print "<tr>\n"
    . "  <th>Key ID</th>\n  <th>Owner</th>\n  <th>Fingerprint</th>\n"
    . "  <th>Size</th>\n  <th>Type</th>\n  <th>Key Info Matches?</th>\n"
    . "  <th>Owner ID Matches?</th>\n";
  foreach my $field (@$extra_fields) {
    print "  <th>$field</th>\n";
  }
  print "</tr>\n";
}

sub get_fingerprints
{
  my $keyring = shift;
  my $cmd = 'gpg2 --fingerprint --no-default-keyring --no-options'
    . " --with-colons --keyring $keyring | egrep "
    . '\'^(pub|fpr|uid):\'';
  my @fps = `$cmd`;
  return \@fps;
}

sub parse_fingerprints
{
  my $fps = shift;
  my $key_metadata = {};
  # This goes like this:
  # pub <-- public key info
  # fpr <-- fingerprint for previous line
  # uid <-- uid
  #     ... possibly many of these
  # sub <-- subkey
  # fpr <-- fingerprint for previous line
  #     ... possibly many sub/fpr pairs
  while (my $line = shift(@{$fps})) {
    if ($line =~ /^pub/) {
      my $have_uid = 0;
      my $have_fpr = 0;
      my ($fingerprint, $owner, $size, $type, $longid, $date, $settrust, $flags);
      # pubkey line
      # pub:-:4096:1:A8B51F5E8032CCE4:1305730074:1590597119::-:::scESC:::::::
      (undef, undef, $size, $type, $longid, $date, undef,
       undef, $settrust, undef, undef, undef, $flags, undef) = split(/:/, $line);
      while (my $next = shift(@{$fps})) {
        if (!$have_fpr && $next =~ /^fpr/) {
          # fpr:::::::::FB298ABBE1D00A1C8FA4DC1FA8B51F5E8032CCE4:
          (undef, undef, undef, undef, undef, undef, undef, undef, undef,
            $fingerprint) = split(/:/, $next);
          $have_fpr = 1;
          next;
        }
        if (!$have_uid && $next =~ /^uid/) {
          (undef, undef, undef, undef, undef, undef, undef, undef, undef,
           $owner) = split(/:/, $next);
          $have_uid = 1;
          last;
        }
      }

      # cribbed from common/openpgpdefs.h in the gnupg source
      # RSA, RSA encrypt-only (legacy), RSA sign-only (legacy)
      if ($type eq '1' || $type eq '2' || $type eq '3') {
        $type = 'RSA';
      # ElGamal encrypt-only
      } elsif ($type eq '16') {
        $type = 'El Gamal';
      } elsif ($type eq '17') {
        $type = 'DSA';
      # RFC-6637, i.e. NISTP
      } elsif ($type eq '18') {
        $type = 'ECDH';
      # RFC-6637, i.e. NISTP
      } elsif ($type eq '19') {
        $type = 'ECDSA';
      # ElGamal encrypt+sign (legacy)
      } elsif ($type eq '20') {
        $type = 'El Gamal';
      } elsif ($type eq '22') {
        $type = 'EDDSA';
      } else {
        $type = 'Unknown';
      }

      if (length($fingerprint) == 40) {
        for my $i (qw(36 32 28 24 20 16 12 8 4)) {
          if ($i != 20) {
            substr($fingerprint, $i, 0, ' ');
          }
          if ($i == 20) {
            substr($fingerprint, $i, 0, "\n");
          }
        }
      } elsif (length($fingerprint) == 32) {
        for my $i (qw(30 28 26 24 22 20 18 16 14 12 10 8 6 4 2)) {
          if ($i != 16) {
            substr($fingerprint, $i, 0, ' ');
          }
          if ($i == 16) {
            substr($fingerprint, $i, 0, "\n");
          }
        }
      }
      $owner =~ s/&/&amp;/;
      $owner =~ s/</&lt\;/;
      $owner =~ s/>/&gt\;/;
  
      push (@{$key_metadata->{$owner}},
            {'id' => $longid,
             'owner' => $owner,
            'fingerprint' => $fingerprint,
            'size' => $size,
            'type' => $type});
    }
  }
  return $key_metadata;
}

sub print_table_body
{
  my ($metadata, $num_fields) = @_;
  # Loop to create extra-fields HTML only once
  my $extra_fields_html = '';
  for (my $i = 0; $i < $num_fields; $i++) {
    $extra_fields_html .= "  <td>&nbsp;</td>\n";
  }
  foreach my $user (sort(keys(%{$metadata}))) {
      foreach my $key (@{$metadata->{$user}}) {
        print "<tr>\n"
          . "  <td><pre>$key->{'id'}</pre></td>\n"
          . "  <td>$key->{'owner'}</td>\n"
          . "  <td><pre>$key->{'fingerprint'}</pre></td>\n"
          . "  <td>$key->{'size'}</td>\n"
          . "  <td>$key->{'type'}</td>\n"
          . "  <td>&nbsp;</td>\n"
          . "  <td>&nbsp;</td>\n"
          . $extra_fields_html
          . "</tr>\n";

      }
  }
}

sub print_html_footer
{
  print "</table>\n</body>\n</html>";
}

sub help
{
  my $err = shift || 0;

  # If we're printing this due to incorrect usage, the user is probably
  # re-directing output to a file, so we need to print to stderr.
  my $fh = *STDOUT;
  if ($err) {
    $fh = *STDERR;
  }
    
  print $fh 'PIUS PGP Keysigning Party Worksheet Generator ' . VERSION . "\n\n";
  print $fh <<EOF;
Usage: $0 <options> <keyring> > out-file.html

<keyring> should be the gpg keyring file with the public keys for all party
participants.

Options:
  -e, --extra-fields <fields>
  A comma-separated list of extra colums to have. This is useful if a subset
  of the participants want to do something extra such as S/MIME for CA Cert
  verification.

  -h, --help
  Print this help message and exit.

  -v, --version
  Print the version and exit.

EOF
}

my $opts = {};
GetOptions($opts,
           'extra-fields|e=s',
           'help|h',
           'version|v',
           ) || die('Bad options');

if (exists($opts->{'help'})) {
  help();
  exit(0);
}

if (exists($opts->{'version'})) {
  print "$0 " . VERSION . "\n";
  exit(0);
}

my $extra_fields = [];
if (exists($opts->{'extra-fields'})) {
  @$extra_fields = split(',', $opts->{'extra-fields'});
}

my $keyring = shift;

unless($keyring) {
  help(1);
  exit(1);
}

my $fps = get_fingerprints($keyring);
my $metadata = parse_fingerprints($fps);
print_html_header($extra_fields);
print_table_body($metadata, scalar(@$extra_fields));
print_html_footer();

