File Coverage

blib/lib/Crypt/PerfectPaperPasswords.pm
Criterion Covered Total %
statement 101 102 99.0
branch 16 26 61.5
condition 5 9 55.5
subroutine 18 18 100.0
pod 5 5 100.0
total 145 160 90.6


line stmt bran cond sub pod time code
1             package Crypt::PerfectPaperPasswords;
2              
3 3     3   38191 use warnings;
  3         7  
  3         101  
4 3     3   16 use strict;
  3         5  
  3         104  
5 3     3   15 use Carp;
  3         5  
  3         295  
6 3     3   3062 use Crypt::Rijndael;
  3         2969  
  3         100  
7 3     3   2849 use Digest::SHA256;
  3         13892  
  3         195  
8 3     3   3763 use Time::HiRes qw(time);
  3         6821  
  3         16  
9 3     3   878 use Scalar::Util qw(refaddr);
  3         7  
  3         561  
10              
11             =head1 NAME
12              
13             Crypt::PerfectPaperPasswords - Steve Gibson's Perfect Paper Passwords
14              
15             =head1 VERSION
16              
17             This document describes Crypt::PerfectPaperPasswords version 0.06
18              
19             =cut
20              
21             our $VERSION = '0.06';
22              
23             =head1 SYNOPSIS
24              
25             use Crypt::PerfectPaperPasswords;
26              
27             my $pass_phrase = 'Fromage';
28             my $ppp = Crypt::PerfectPaperPasswords->new;
29             my $sequence_key = $ppp->sequence_from_key( $pass_phrase );
30             my $first = 1;
31             my $count = 100;
32             my @passcodes = $ppp->passcodes( $first, $count, $sequence_key );
33              
34             =head1 DESCRIPTION
35              
36             From L
37              
38             GRC's "Perfect Paper Passwords" (PPP) system is a straightforward,
39             simple and secure implementation of a paper-based One Time Password
40             (OTP) system. When used in conjunction with an account name &
41             password, the individual "passcodes" contained on PPP's "passcards"
42             serve as the second factor ("something you have") of a secure multi-
43             factor authentication system.
44              
45             This is a Perl implementation of the PPP passcode generator.
46              
47             =head1 INTERFACE
48              
49             =head2 C<< new >>
50              
51             Create a new C instance. Options may
52             be passed:
53              
54             my $ppp = Crypt::PerfectPaperPasswords->new(
55             alphabet => '0123456789abcdef',
56             codelen => 2
57             );
58              
59             The following options are supported:
60              
61             =over
62              
63             =item C
64              
65             The alphabet to use for encoding. Defaults to Steve Gibson's:
66              
67             23456789!@#%+=:?abcdefghijkmnopq
68             rstuvwxyzABCDEFGHJKLMNPRSTUVWXYZ
69              
70             The size of the alphabet need not be a power of two.
71              
72             =item C
73              
74             The number of raw bytes in each passcode. You must have L
75             installed to handle values greater than 4.
76              
77             =back
78              
79             =cut
80              
81             {
82             my %DEFAULT_ARGS;
83              
84             BEGIN {
85 3     3   23 %DEFAULT_ARGS = (
86             alphabet => '23456789!@#%+=:?'
87             . 'abcdefghijkmnopqrstuvwxyz'
88             . 'ABCDEFGHJKLMNPRSTUVWXYZ',
89             codelen => 3,
90             );
91              
92 3         13 for my $method ( keys %DEFAULT_ARGS ) {
93 3     3   16 no strict 'refs';
  3         5  
  3         345  
94 6         4175 *{ __PACKAGE__ . '::' . $method } = sub {
95 493     493   1091 my $self = shift;
96 493 50       1091 croak "Can't set $method" if @_;
97 493         6443 return $self->{$method};
98 6         44 };
99             }
100             }
101              
102             sub new {
103 8     8 1 9411 my $class = shift;
104 8         56 my %args = ( %DEFAULT_ARGS, @_ );
105              
106 8         24 my $alphabet = delete $args{alphabet};
107              
108 8 50       32 croak "Alphabet must be at least two characters long"
109             unless length( $alphabet ) >= 2;
110              
111 8         18 my %got = ();
112 8         346 $got{$_}++ for split //, $alphabet;
113 8         87 my @dups = sort grep { $got{$_} > 1 } keys %got;
  364         544  
114 8 50       44 croak "Duplicate characters in alphabet: ", join( ', ', @dups )
115             if @dups;
116              
117 8         19 my $codelen = delete $args{codelen};
118              
119 8 50 33     48 croak "Code length must be between 1 and 32"
120             if $codelen < 1 || $codelen > 32;
121              
122 8 50 66     27 if ( $codelen > 4 && !_got_bigint() ) {
123 0         0 croak "Please install Math::BigInt to handle code lengths > 4";
124             }
125              
126 8         70 my $self = bless {
127             alphabet => $alphabet,
128             codelen => $codelen,
129             seed => time(),
130             }, $class;
131              
132 8 50       20 croak "Unknown options: ", join( ', ', sort keys %args ), "\n"
133             if keys %args;
134              
135 8         72 return $self;
136             }
137             }
138              
139             =head2 C<< alphabet >>
140              
141             Get the alphabet used by this object.
142              
143             my $alphabet = $ppp->alphabet;
144              
145             =head2 C<< codelen >>
146              
147             Get the code length for this object.
148              
149             my $codelen = $ppp->codelen;
150              
151             =head2 C<< sequence_from_key >>
152              
153             Generate a sequence key from a passphrase.
154              
155             my $seq_key = $ppp->sequence_from_key( 'Fromage' );
156              
157             =cut
158              
159             sub sequence_from_key {
160 9     9 1 42 my $self = shift;
161 9         14 my $key = shift;
162              
163 9         59 my $sha = Digest::SHA256::new( 256 );
164 9         55 $sha->add( $key );
165 9         40 my $digest = $sha->hexdigest;
166 9         320 $digest =~ s/\s+//g;
167 9         47 return $digest;
168             }
169              
170             =head2 C<< random_sequence >>
171              
172             Generate a random sequence key.
173              
174             my $seq_key = $ppp->random_sequence;
175              
176             Relies on the output of C for its entropy.
177              
178             =cut
179              
180             sub random_sequence {
181 2     2 1 691 my $self = shift;
182 2         5 return $self->sequence_from_key( $self->random_data );
183             }
184              
185             =head2 C<< random_data >>
186              
187             Returns some random data. This is the entropy source for
188             C. This implementation returns a string
189             that is the concatenation of
190              
191             =over
192              
193             =item * The real time (using the microsecond clock)
194              
195             =item * The next seed value
196              
197             =item * Address of C<$self>
198              
199             =item * Address of a newly allocated scalar
200              
201             =item * Process ID
202              
203             =back
204              
205             The seed value is the microsecond time when this object was created and
206             is incremented by one each time it's used.
207              
208             For a lot of uses this is probably an adequate entropy source - but I'm
209             not a cryptographer. If you'd like better entropy consider subclassing
210             and provding a C that reads from /dev/urandom.
211              
212             =cut
213              
214             sub random_data {
215 2     2 1 4 my $self = shift;
216 2         50 return join( ':',
217             time(), $self->{seed}++,
218             refaddr( $self ),
219             refaddr( \my $dummy ), $$ );
220             }
221              
222             =head2 C<< passcodes >>
223              
224             Get an array of passcodes.
225              
226             my @passcodes = $ppp->passcodes(1, 70, $seq_key);
227              
228             The first two arguments are the starting position (1 .. n) and the
229             number of passcodes to generate.
230              
231             Returns an array of strings containing the generated passcodes.
232              
233             =cut
234              
235             sub passcodes {
236 7 50   7 1 3692 croak "passcodes requires 3 args" unless @_ == 4;
237 7         15 my ( $self, $first, $count, $sequence ) = @_;
238              
239 7 50       21 croak "Sequence must be 64 characters long"
240             unless length( $sequence ) == 64;
241              
242 7         11 my @passcodes = ();
243              
244 7 50       14 croak "Starting index is 1" if $first <= 0;
245 7         9 $first--;
246              
247 7         13 $first *= $count;
248              
249 7         20 my $codelen = $self->codelen;
250              
251 7         127 my $rij = Crypt::Rijndael->new( pack( 'H*', $sequence ),
252             Crypt::Rijndael::MODE_ECB );
253              
254 7         20 while ( @passcodes < $count ) {
255 485         2984 my $pos = $first * 8 * $codelen;
256 485         660 my $n = $pos / 128;
257 485         677 my $offset = $pos % 128;
258 485         998 my $desired = int( $offset / 8 ) + $codelen;
259 485         812 my $raw = '';
260              
261 485         733 for my $j ( 0 .. 1 ) {
262 551         3022 my $n_bits = pack( "V*", "$n" ); # $n_bits .= ;
263 551         3500 $raw .= $rij->encrypt(
264             $n_bits . "\0" x ( 16 - length( $n_bits ) % 16 ) );
265 551 100       1783 last if length( $raw ) >= $desired;
266 66         181 $n++;
267             }
268              
269 485         1388 push @passcodes,
270             $self->_alpha_encode( substr( $raw, $offset / 8, $codelen ),
271             $codelen );
272              
273 485         1607 $first++;
274             }
275              
276 7         217 return @passcodes;
277             }
278              
279             {
280             my $GOT_BIGINT;
281              
282             sub _got_bigint {
283 486 100   486   2620 defined $GOT_BIGINT and return $GOT_BIGINT;
284 1 50   1   89 return $GOT_BIGINT = eval 'use Math::BigInt; 1' ? 1 : 0;
  1         1777  
  1         22896  
  1         7  
285             }
286             }
287              
288             sub _alpha_encode {
289 485     485   1284 my ( $self, $data, $bytes ) = @_;
290 485         523 my $code;
291              
292 485 100 66     811 if ( _got_bigint() && $bytes > 4 ) {
293             # Make a big hex constant
294 210         860 $code = Math::BigInt->new(
295             '0x'
296             . join( '',
297 35         124 map { sprintf( "%02x", ord( $_ ) ) } reverse split //, $data )
298             );
299             }
300             else {
301 450         1030 $code = unpack( 'V', $data . "\0" x ( 4 - length $data ) );
302             }
303              
304 485         6487 my $limit = 2**( $bytes * 8 );
305              
306 485         1210 my @alphabet = split //, $self->alphabet;
307 485         1518 my $code_space = @alphabet;
308 485         845 my @out = ();
309 485         603 my $max = 1;
310              
311 485         1079 while ( $max < $limit ) {
312 2850         6067 push @out, $alphabet[ $code % $code_space ];
313 2850         51492 $code = int( $code / $code_space );
314 2850         52392 $max *= $code_space;
315             }
316              
317 485         4680 return join '', @out;
318             }
319              
320             1;
321             __END__