#! /usr/bin/env perl
use v5.12;
use warnings;
use Pod::Usage;
use Getopt::Long qw( :config gnu_getopt );
use Crypt::SecretBuffer qw/ secret span /;
use Crypt::MultiKey::PKey;
use Crypt::MultiKey::Coffer;

=head1 USAGE

  cmk-coffer-new [OPTIONS] COFFER_FILE -a PKEY_FILE [...] [-a ...]

Create COFFER_FILE and encrypt it such that each access group of PKeys can be
used independenctly to unlock it.  The data to be encrypted is read from stdin,
but may also be supplied by C<--value> or one or more C<--dict-pair> options.

=head1 OPTIONS

=over

=item --bundled-keys (-b)

Serialize all PKey objects into the tail of the COFFER_FILE so that the key
files don't need referenced again to unlock it.

=item --add-access (-a) PKEY [PKEY...]

The arguments following this option must be one or more PKey filenames.
Access to the coffer will be granted when the private halves of all of these
keys are available.  This option may be specified multiple times, to create
multiple sets of keys where each has access to the Coffer.  A Pkey may be used
in more than one group.

=item --parse=FORMAT

Parse names and values from stdin and store them as dictionary content.

Formats:

=over

=item ini

Parse input as lines of C<NAME=VALUE> in the tradition of INI files, where ';'
and '#' begin comments and whitespace is trimmed from the names and values.
Bare names (lacking an '=') are not supported, nor are multiline values.
INI headers are currently ignored.

=item 0

Parse the input as NUL-delimited strings alternating between Name and Value.

=back

=item --dict-item (-d) NAME=VALUE_FILE

If specified, the Coffer will be created as dictionary storage, and this
specifies one entry.  NAME cannot contain an equal sign.  Value may contain
arbitrary binary data including NUL bytes.  This option can be used multiple
times, and applies after items generated by C<--parse>.  If option C<--parse>
is not used, C<--dict-item> implies that no value will be read from stdin.

=item --content-type MIMETYPE

Optionally specify a MIME-type for the content of the Coffer, stored in the
public headers (not encrypted).
Dictionary storage implies its own content type, so no need to set this if
you specify dictionary storage options.

=back

=cut

my @flists= ([]);
GetOptions(
   'bundled-keys|b'        => \my $opt_bundled_keys,
   'content-type=s'        => \my $opt_content_type,
   'parse=s'               => \my $opt_parse,
   'dict-item|d=s'         => \my @opt_dict_item,
   'add-access|a'          => sub { push @flists, [] },
   '<>'                    => sub { push @{$flists[-1]}, $_[0] },
   'help'                  => sub { pod2usage(1) },
) or pod2usage(2);

unless (@{$flists[0]} == 1) {
   pod2usage(-message => 'Coffer filename must preceed "--access"')
      unless @{$flists[0]};
   pod2usage(-message => 'Unexpected non-option argument following Coffer filename');
}
my $path= (shift @flists)->[0];
pod2usage(-message => "Path '$path' already exists")
   if -e $path;

my $in;
if ($opt_parse || !@opt_dict_item) {
   $in= secret;
   while ($in->append_sysread(\*STDIN, 4096) // die "read(stdin): $!\n") {}
}

my $coffer= Crypt::MultiKey::Coffer->new(
   path => $path,
   bundled_keys => $opt_bundled_keys,
   content_type => $opt_content_type,
);

if (defined $opt_parse || @opt_dict_item) {
   # Dictionary storage.
   if (defined $opt_parse) {
      my $span= span($in);
      if ($opt_parse eq '0') {
         while ($span->len) {
            my $k= $span->parse(qr/[^\0]/)
               // die "Encountered 0-length dict name in input\n";
            my $v= $span->ltrim("\0")->parse(qr/[^\0]/);
            $span->ltrim("\0");
            $coffer->set($k, $v // '');
         }
      }
      elsif ($opt_parse eq 'ini') {
         require Crypt::SecretBuffer::INI;
         my $ini= Crypt::SecretBuffer::INI->new(inline_comments => 1);
         while ($span->len) {
            my $attrs= $ini->parse_next($span);
            die "Parse error on input: $attrs->{error}\n"
               if defined $attrs->{error};
            die "INI headers are not permitted\n"
               if defined $attrs->{section};
            next unless defined $attrs->{key};
            $coffer->set($attrs->{key}, $attrs->{value} // '');
         }
      }
      else {
         die "Unknown parse format '$opt_parse'\n";
      }
   }
   for (@opt_dict_item) {
      my ($k, $file)= split /=/, $_, 2;
      defined $file && length $file
         or die "--dict-item $k lacks a filename";
      my $val= secret(load_file => $file); # dies on failure
      $coffer->set($k, $val);
   }
} else {
   # stdin is the content of the coffer
   $coffer->content($in);
}

pod2usage(-message => 'Require one or more --add-access options')
   unless @flists;
for my $pkey_files (@flists) {
   pod2usage(-message => 'Empty access list.  Require one or more PKey file names')
      unless @$pkey_files;
   my @pkeys= map Crypt::MultiKey::PKey->load($_), @$pkey_files;
   $coffer->add_access(@pkeys);
}

$coffer->save;

exit 0;
