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 Crypt::PerfectPaperPasswords;

use warnings;
use strict;
use Carp;
use Crypt::Rijndael;
use Digest::SHA256;
use Time::HiRes qw(time);
use Scalar::Util qw(refaddr);

=head1 NAME

Crypt::PerfectPaperPasswords - Steve Gibson's Perfect Paper Passwords

=head1 VERSION

This document describes Crypt::PerfectPaperPasswords version 0.06

=cut

our $VERSION = '0.06';

=head1 SYNOPSIS

    use Crypt::PerfectPaperPasswords;

    my $pass_phrase  = 'Fromage';
    my $ppp          = Crypt::PerfectPaperPasswords->new;
    my $sequence_key = $ppp->sequence_from_key( $pass_phrase );
    my $first        = 1;
    my $count        = 100;
    my @passcodes    = $ppp->passcodes( $first, $count, $sequence_key );

=head1 DESCRIPTION

From L<https://www.grc.com/ppp.htm>

    GRC's "Perfect Paper Passwords" (PPP) system is a straightforward,
    simple and secure implementation of a paper-based One Time Password
    (OTP) system. When used in conjunction with an account name &
    password, the individual "passcodes" contained on PPP's "passcards"
    serve as the second factor ("something you have") of a secure multi-
    factor authentication system.

This is a Perl implementation of the PPP passcode generator.

=head1 INTERFACE 

=head2 C<< new >>

Create a new C<Create::PerfectPaperPasswords> instance. Options may
be passed:

    my $ppp = Crypt::PerfectPaperPasswords->new(
        alphabet => '0123456789abcdef',
        codelen  => 2
    );

The following options are supported:

=over

=item C<alphabet>

The alphabet to use for encoding. Defaults to Steve Gibson's:

    23456789!@#%+=:?abcdefghijkmnopq
    rstuvwxyzABCDEFGHJKLMNPRSTUVWXYZ

The size of the alphabet need not be a power of two.

=item C<codelen>

The number of raw bytes in each passcode. You must have L<Math::BigInt>
installed to handle values greater than 4.

=back

=cut

{
  my %DEFAULT_ARGS;

  BEGIN {
    %DEFAULT_ARGS = (
      alphabet => '23456789!@#%+=:?'
       . 'abcdefghijkmnopqrstuvwxyz'
       . 'ABCDEFGHJKLMNPRSTUVWXYZ',
      codelen => 3,
    );

    for my $method ( keys %DEFAULT_ARGS ) {
      no strict 'refs';
      *{ __PACKAGE__ . '::' . $method } = sub {
        my $self = shift;
        croak "Can't set $method" if @_;
        return $self->{$method};
      };
    }
  }

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

    my $alphabet = delete $args{alphabet};

    croak "Alphabet must be at least two characters long"
     unless length( $alphabet ) >= 2;

    my %got = ();
    $got{$_}++ for split //, $alphabet;
    my @dups = sort grep { $got{$_} > 1 } keys %got;
    croak "Duplicate characters in alphabet: ", join( ', ', @dups )
     if @dups;

    my $codelen = delete $args{codelen};

    croak "Code length must be between 1 and 32"
     if $codelen < 1 || $codelen > 32;

    if ( $codelen > 4 && !_got_bigint() ) {
      croak "Please install Math::BigInt to handle code lengths > 4";
    }

    my $self = bless {
      alphabet => $alphabet,
      codelen  => $codelen,
      seed     => time(),
    }, $class;

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

    return $self;
  }
}

=head2 C<< alphabet >>

Get the alphabet used by this object.

    my $alphabet = $ppp->alphabet;

=head2 C<< codelen >>

Get the code length for this object.

    my $codelen = $ppp->codelen;

=head2 C<< sequence_from_key >>

Generate a sequence key from a passphrase.

    my $seq_key = $ppp->sequence_from_key( 'Fromage' );

=cut

sub sequence_from_key {
  my $self = shift;
  my $key  = shift;

  my $sha = Digest::SHA256::new( 256 );
  $sha->add( $key );
  my $digest = $sha->hexdigest;
  $digest =~ s/\s+//g;
  return $digest;
}

=head2 C<< random_sequence >>

Generate a random sequence key.

    my $seq_key = $ppp->random_sequence;

Relies on the output of C<random_data> for its entropy.

=cut

sub random_sequence {
  my $self = shift;
  return $self->sequence_from_key( $self->random_data );
}

=head2 C<< random_data >>

Returns some random data. This is the entropy source for
C<random_sequence>. This implementation returns a string
that is the concatenation of

=over

=item * The real time (using the microsecond clock)

=item * The next seed value

=item * Address of C<$self>

=item * Address of a newly allocated scalar

=item * Process ID

=back

The seed value is the microsecond time when this object was created and
is incremented by one each time it's used.

For a lot of uses this is probably an adequate entropy source - but I'm
not a cryptographer. If you'd like better entropy consider subclassing
and provding a C<random_data> that reads from /dev/urandom.

=cut

sub random_data {
  my $self = shift;
  return join( ':',
    time(), $self->{seed}++,
    refaddr( $self ),
    refaddr( \my $dummy ), $$ );
}

=head2 C<< passcodes >>

Get an array of passcodes.

    my @passcodes = $ppp->passcodes(1, 70, $seq_key);

The first two arguments are the starting position (1 .. n) and the
number of passcodes to generate.

Returns an array of strings containing the generated passcodes.

=cut

sub passcodes {
  croak "passcodes requires 3 args" unless @_ == 4;
  my ( $self, $first, $count, $sequence ) = @_;

  croak "Sequence must be 64 characters long"
   unless length( $sequence ) == 64;

  my @passcodes = ();

  croak "Starting index is 1" if $first <= 0;
  $first--;

  $first *= $count;

  my $codelen = $self->codelen;

  my $rij = Crypt::Rijndael->new( pack( 'H*', $sequence ),
    Crypt::Rijndael::MODE_ECB );

  while ( @passcodes < $count ) {
    my $pos     = $first * 8 * $codelen;
    my $n       = $pos / 128;
    my $offset  = $pos % 128;
    my $desired = int( $offset / 8 ) + $codelen;
    my $raw     = '';

    for my $j ( 0 .. 1 ) {
      my $n_bits = pack( "V*", "$n" );    # $n_bits .= ;
      $raw .= $rij->encrypt(
        $n_bits . "\0" x ( 16 - length( $n_bits ) % 16 ) );
      last if length( $raw ) >= $desired;
      $n++;
    }

    push @passcodes,
     $self->_alpha_encode( substr( $raw, $offset / 8, $codelen ),
      $codelen );

    $first++;
  }

  return @passcodes;
}

{
  my $GOT_BIGINT;

  sub _got_bigint {
    defined $GOT_BIGINT and return $GOT_BIGINT;
    return $GOT_BIGINT = eval 'use Math::BigInt; 1' ? 1 : 0;
  }
}

sub _alpha_encode {
  my ( $self, $data, $bytes ) = @_;
  my $code;

  if ( _got_bigint() && $bytes > 4 ) {
    # Make a big hex constant
    $code = Math::BigInt->new(
      '0x'
       . join( '',
        map { sprintf( "%02x", ord( $_ ) ) } reverse split //, $data )
    );
  }
  else {
    $code = unpack( 'V', $data . "\0" x ( 4 - length $data ) );
  }

  my $limit = 2**( $bytes * 8 );

  my @alphabet   = split //, $self->alphabet;
  my $code_space = @alphabet;
  my @out        = ();
  my $max        = 1;

  while ( $max < $limit ) {
    push @out, $alphabet[ $code % $code_space ];
    $code = int( $code / $code_space );
    $max *= $code_space;
  }

  return join '', @out;
}

1;
__END__

=head1 CONFIGURATION AND ENVIRONMENT
  
Crypt::PerfectPaperPasswords requires no configuration files or environment variables.

=head1 DEPENDENCIES

L<Crypt::Rijndael>

L<Digest::SHA256>

L<Scalar::Util>

L<Time::HiRes>

L<Math::BigInt> (optional)


=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>.