The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.
package App::PerlPPP;

use strict;
use warnings;
use Carp;
use Getopt::Long;
use Crypt::PerfectPaperPasswords;
use Pod::Usage;

=head1 NAME

App::PerlPPP - Command line tool for Perfect Paper Passwords

=head1 VERSION

This document describes App::PerlPPP version 0.06

=cut

our $VERSION = '0.06';

=head1 SYNOPSIS

    use App::PerlPPP;

    my $app = App::PerlPPP->new;
    $app->parse_args(@ARGV);
    $app->run;

=head1 DESCRIPTION

=head1 INTERFACE 

=head2 C<< new >>

=cut

{
  my %ARG_SPEC;

  BEGIN {

    sub _range_spec {
      my ( $name, $default, $min, $max ) = @_;
      return [
        $default,
        sub {
          my ( $self, $value ) = @_;
          die "$name must be between $min and $max\n"
           if $value < $min || $value > $max;
          return $value;
         }
      ];
    }

    sub _array_spec {
      return [
        [],
        sub {
          my $self = shift;
          return [ map { 'ARRAY' eq ref $_ ? @$_ : $_ } @_ ];
        },
      ];
    }

    %ARG_SPEC = (
      show_man   => [0],
      show_help  => [0],
      args       => _array_spec(),
      columns    => _range_spec( 'columns', 7, 1, 100 ),
      rows       => _range_spec( 'rows', 10, 1, 100 ),
      codelen    => [undef],
      passphrase => [undef],
      alphabet   => [undef],
      key        => [
        undef,
        sub {
          my ( $self, $key ) = @_;
          die "Key must be 64 characters long\n"
           unless length( $key ) == 64;
          die "Key must be hexadecimal (0-9, A-F)\n"
           unless $key =~ /^[0-9A-F]{64}$/i;
          return $key;
        },
      ],
      title => ['Perfect Paper Passwords'],
    );

    while ( my ( $name, $spec ) = each %ARG_SPEC ) {
      no strict 'refs';
      my $validator = $spec->[1] || sub { shift; shift };
      *{ __PACKAGE__ . '::' . $name } = sub {
        my $self = shift;
        $self->{$name} = $self->$validator( @_ )
         if ( @_ );
        my $value = $self->{$name};
        return ( wantarray && 'ARRAY' eq ref $value )
         ? @$value
         : $value;
      };
    }
  }

  sub new {
    my ( $class, %args ) = @_;

    my $self = bless {}, $class;

    while ( my ( $name, $spec ) = each %ARG_SPEC ) {
      my $value
       = exists $args{$name} ? delete $args{$name} : $spec->[0];
      $self->$name( $value )
       if defined $value;
    }

    croak "Unknown options: ", join( ', ', sort keys %args )
     if keys %args;

    return $self;
  }
}

=head2 C<< args >>

=head2 C<< alphabet >>

=head2 C<< codelen >>

=head2 C<< columns >>

=head2 C<< rows >>

=head2 C<< key >>

=head2 C<< passphrase >>

=head2 C<< title >>

=head2 C<< show_help >>

=head2 C<< show_man >>

=head2 C<< parse_args >>

=cut

sub parse_args {
  my ( $self, @args ) = @_;

  local @ARGV = @args;

  my %options;

  GetOptions(
    'help|?'       => \$options{show_help},
    man            => \$options{show_man},
    'key=s'        => \$options{key},
    'passphrase=s' => \$options{passphrase},
    'columns=i'    => \$options{columns},
    'rows=i'       => \$options{rows},
    'title=s'      => \$options{title},
    'alphabet=s'   => \$options{alphabet},
    'codelen=i'    => \$options{codelen},
  ) or pod2usage();

  while ( my ( $name, $value ) = each %options ) {
    $self->$name( $value ) if defined $value;
  }

  $self->args( @ARGV );
}

=head2 C<< run >>

=cut

sub run {
  my $self = shift;

  if ( $self->show_help ) {
    $self->do_help;
  }
  elsif ( $self->show_man ) {
    pod2usage( -verbose => 2, -exitstatus => 0 );
  }
  else {
    my @args = $self->args;
    pod2usage() unless @args;
    my $verb = shift @args;
    if ( my $code = $self->can( "do_$verb" ) ) {
      $self->$code( @args );
    }
    else {
      die "Unknown action: $verb\n";
    }
  }
}

=head1 ACTIONS

=head2 C<< do_card >>

Output a card

=cut

sub do_card {
  my ( $self, @args ) = @_;
  my $card_no = @args ? shift @args : 1;
  die "Card numbers start at 1\n" if $card_no < 1;

  my $title = $self->title;
  my $rows  = $self->rows;
  my $cols  = $self->columns;
  my $ppp   = $self->_make_ppp;
  my @passcodes
   = $ppp->passcodes( $card_no, $rows * $cols, $self->_get_key );

  my $colw   = length( $passcodes[0] );
  my $center = sub {
    my $str = shift;
    my $pad = $colw - length $str;
    return $str if $pad <= 0;
    return ( ' ' x ( $pad / 2 ) ) . $str
     . ( ' ' x ( ( $pad + 1 ) / 2 ) );
  };

  my $row_fmt = "%4d";
  my @hdr     = ( ' ' x ( length sprintf( $row_fmt, 1 ) ) );
  my $col     = 'A';
  push @hdr, $center->( $col++ ) for ( 1 .. $cols );
  my $hdr = join( ' ', @hdr );
  my $rule = '=' x length $hdr;
  print "$title [$card_no]\n$rule\n$hdr\n$rule\n";
  for ( 1 .. $rows ) {
    print join( ' ',
      sprintf( $row_fmt, $_ ),
      splice( @passcodes, 0, $cols ) ),
     "\n";
  }
  print "$rule\n";
}

=head2 C<do_newkey>

Create and display a new random key.

=cut

sub do_newkey {
  my $self = shift;
  if ( my $key = $self->_make_key ) {
    print "Specified key is $key\n";
  }
  else {
    print "Generated key is ",
     $self->key( $self->_make_ppp->random_sequence ), "\n";
  }
}

=head2 C<do_help>

Output help page

=cut

sub do_help {
  my $self = shift;
  pod2usage( -verbose => 1 );
}

sub _make_key {
  my $self = shift;

  if ( defined( my $key = $self->key ) ) {
    return $key;
  }
  elsif ( defined( my $phrase = $self->passphrase ) ) {
    return $self->key( $self->_make_ppp->sequence_from_key( $phrase ) );
  }
  else {
    return;
  }
}

sub _get_key {
  my $self = shift;
  return $self->_make_key
   || die "Must supply --key or --passphrase\n";
}

sub _make_ppp {
  my $self = shift;
  my %args;
  for my $a ( qw( alphabet codelen ) ) {
    if ( defined( my $value = $self->$a() ) ) {
      $args{$a} = $value;
    }
  }

  return $self->{_ppp} ||= Crypt::PerfectPaperPasswords->new( %args );
}

1;
__END__

=head1 CONFIGURATION AND ENVIRONMENT
  
App::PerlPPP requires no configuration files or environment variables.

=head1 DEPENDENCIES

L<Crypt::PerfectPaperPasswords>

=head1 INCOMPATIBILITIES

None reported.

=head1 BUGS AND LIMITATIONS

No bugs have been reported.

Please report any bugs or feature requests to
C<bug-crypt-perfectpaperpasswords@rt.cpan.org>, or through the web interface at
L<http://rt.cpan.org>.

=head1 AUTHOR

Andy Armstrong  C<< <andy@hexten.net> >>

Original Perfect Paper Passwords implementation by Steve Gibson. More details
here:

    http://www.grc.com/ppp.htm

=head1 LICENCE AND COPYRIGHT

Copyright (c) 2007, Andy Armstrong C<< <andy@hexten.net> >>.

This module is free software; you can redistribute it and/or
modify it under the same terms as Perl itself. See L<perlartistic>.