File Coverage

blib/lib/Authen/TOTP.pm
Criterion Covered Total %
statement 137 191 71.7
branch 45 88 51.1
condition 26 49 53.0
subroutine 22 27 81.4
pod 3 20 15.0
total 233 375 62.1


line stmt bran cond sub pod time code
1             # Authen::TOTP version 0.1.1
2             #
3             # Copyright (c) 2020 Thanos Chatziathanassiou . All rights reserved.
4             # This program is free software; you can redistribute it and/or
5             # modify it under the same terms as Perl itself.
6              
7             package Authen::TOTP;
8             local $^W;
9             require 'Exporter.pm';
10 2     2   223974 use vars qw(@ISA @EXPORT @EXPORT_OK);
  2         2  
  2         209  
11             @ISA = (Exporter);
12              
13             @EXPORT_OK = qw();
14              
15             $Authen::TOTP::VERSION='0.1.1';
16             $Authen::TOTP::ver=$Authen::TOTP::VERSION;
17              
18 2     2   12 use strict;
  2         3  
  2         74  
19 2     2   11 use warnings;
  2         3  
  2         105  
20 2     2   879 use utf8;
  2         442  
  2         14  
21 2     2   52 use Carp;
  2         3  
  2         102  
22 2     2   1090 use Data::Dumper;
  2         14905  
  2         195  
23 2     2   836 use Crypt::PRNG;
  2         6971  
  2         5884  
24              
25             sub debug_print {
26 0     0 0 0 my $self = shift;
27            
28             #fancy stuff that can be done later
29 0         0 warn @_;
30            
31 0         0 return 1;
32             }
33              
34             sub process_sub_arguments {
35 36     36 0 41 my $self = shift;
36              
37 36         36 my $args = shift;
38 36         33 my $wants = shift;
39 36         45 my @rets;
40              
41 36 50       85 if (@$args != 0) {
42 36 50       83 if (ref $args->[0] eq 'HASH') {
    0          
43 36         55 foreach my $want (@$wants) {
44 288         402 push @rets,$args->[0]->{$want};
45             }
46             }
47             elsif (!(scalar(@$args)%2)) {
48 0         0 my %hash = @$args;
49 0         0 foreach my $want (@$wants) {
50 0         0 push @rets,$hash{$want};
51             }
52             }
53             }
54 36         128 return @rets;
55             }
56              
57             sub valid_digits {
58 55     55 0 59 my $self = shift;
59 55         55 my $digits = shift;
60              
61 55 100 66     244 if ($digits && $digits =~ m|^[68]$|) {
    100 66        
62 36         61 $self->{digits} = $digits;
63             }
64             elsif (!defined($self->{digits}) || $self->{digits} !~ m|^[68]$|) {
65 1         3 $self->{digits} = 6;
66             }
67 55         67 1;
68             }
69             sub valid_period {
70 55     55 0 58 my $self = shift;
71 55         77 my $period = shift;
72              
73 55 50 33     244 if ($period && $period =~ m|^[36]0$|) {
    100 66        
74 0         0 $self->{period} = $period;
75             }
76             elsif (!defined($self->{period}) || $self->{period} !~ m|^[36]0$|) {
77 19         49 $self->{period} = 30;
78             }
79 55         61 1;
80             }
81             sub valid_algorithm {
82 75     75 0 73 my $self = shift;
83 75         100 my $algorithm = shift;
84              
85 75 100 66     304 if ($algorithm && $algorithm =~ m|^SHA\d+$|) {
    100 66        
86 20         30 $self->{algorithm} = $algorithm;
87             }
88             elsif (!defined($self->{algorithm}) || $self->{algorithm} !~ m|^SHA\d+$|) {
89 19         31 $self->{algorithm} = "SHA1";
90             }
91 75         78 1;
92             }
93             sub valid_when {
94 55     55 0 50 my $self = shift;
95 55         63 my $when = shift;
96              
97 55 100 66     236 if ($when && $when =~ m|^\-?\d+$|) { #negative epoch is valid, though not sure how useful :)
    100 66        
98 36         42 $self->{when} = $when;
99             }
100             elsif (!defined($self->{when}) || $self->{when} !~ m|^\-?\d+$|) {
101 1         2 $self->{when} = time;
102             }
103 55         60 1;
104             }
105             sub valid_tolerance {
106 55     55 0 52 my $self = shift;
107 55         54 my $tolerance = shift;
108              
109 55 100 66     222 if ($tolerance && $tolerance =~ m|^\d+$| && $tolerance > 0) {
    100 66        
      66        
110 36         50 $self->{tolerance} = ($tolerance-1);
111             }
112             elsif (!defined($self->{tolerance}) || $self->{tolerance} !~ m|^\d+$|) {
113 1         2 $self->{tolerance} = 0;
114             }
115 55         56 1;
116             }
117             sub valid_secret {
118 73     73 0 100 my $self = shift;
119 73         128 my ($secret, $base32secret) = @_;
120              
121 73 100       94 if ($secret) {
    50          
122 54         66 $self->{secret} = $secret;
123             }
124             elsif ($base32secret) {
125 0         0 $self->{secret} = $self->base32dec($base32secret);
126             }
127             else {
128 19 50       26 if (defined($self->{base32secret})) {
129 0         0 $self->{secret} = $self->base32dec($self->{base32secret});
130             }
131             else {
132 19 50       31 if (defined($self->{algorithm})) {
133 19 50       66 if ($self->{algorithm} eq "SHA512") {
    50          
134 0         0 $self->{secret} = $self->gen_secret(64);
135             }
136             elsif ($self->{algorithm} eq "SHA256") {
137 0         0 $self->{secret} = $self->gen_secret(32);
138             }
139             else {
140 19         36 $self->{secret} = $self->gen_secret(20);
141             }
142             }
143             else {
144 0         0 $self->{secret} = $self->gen_secret(20);
145             }
146             }
147             }
148            
149 73         122 $self->{base32secret} = $self->base32enc($self->{secret});
150 73         4724 1;
151             }
152             sub secret {
153 0     0 1 0 my $self = shift;
154 0         0 return $self->{secret};
155             }
156             sub base32secret {
157 0     0 1 0 my $self = shift;
158 0         0 return $self->{base32secret};
159             }
160             sub algorithm {
161 20     20 1 591 my $self = shift;
162 20         64 my $algorithm = shift;
163 20         44 $self->valid_algorithm($algorithm);
164              
165 20         29 return $self->{algorithm};
166             }
167              
168             sub hmac {
169 54     54 0 48 my $self = shift;
170 54         63 my $Td = shift;
171              
172 54 50 50     54 if ((eval {require Digest::SHA;1;} || 0) ne 1) {
173             # if module can't load
174 0         0 require Digest::SHA::PurePerl;
175 0 0       0 $self->{DEBUG} and $self->debug_print("Digest::SHA unavailable, using Digest::SHA::PurePerl()");
176 0 0       0 if ($self->{algorithm} eq 'SHA512') {
    0          
177 0         0 return Digest::SHA::PurePerl::hmac_sha512_hex($Td, $self->{secret});
178             }
179             elsif ($self->{algorithm} eq 'SHA256') {
180 0         0 return Digest::SHA::PurePerl::hmac_sha256_hex($Td, $self->{secret} );
181             }
182             else {
183 0         0 return Digest::SHA::PurePerl::hmac_sha1_hex($Td, $self->{secret} );
184             }
185             }
186             else {
187             #we have XS!
188 54 50       86 $self->{DEBUG} and $self->debug_print("using Digest::SHA()");
189 54 100       104 if ($self->{algorithm} eq 'SHA512') {
    100          
190 18         176 return Digest::SHA::hmac_sha512_hex($Td, $self->{secret});
191             }
192             elsif ($self->{algorithm} eq 'SHA256') {
193 18         180 return Digest::SHA::hmac_sha256_hex($Td, $self->{secret} );
194             }
195             else {
196 18         165 return Digest::SHA::hmac_sha1_hex($Td, $self->{secret} );
197             }
198             }
199             }
200              
201             sub base32enc {
202 73     73 0 99 my $self = shift;
203              
204 73 50 50     81 if ((eval {require Encode::Base2N;1;} || 0) ne 1) {
205             # if module can't load
206 73         696 require MIME::Base32;
207 73 50       1255 $self->{DEBUG} and $self->debug_print("Encode::Base2N unavailable, using MIME::Base32()");
208 73         172 return MIME::Base32::encode_base32(shift);
209             }
210             else {
211             #we have XS!
212 0 0       0 $self->{DEBUG} and $self->debug_print("using Encode::Base2N()");
213 0         0 return Encode::Base2N::encode_base32(shift);
214             }
215             }
216              
217             sub base32dec {
218 0     0 0 0 my $self = shift;
219              
220 0 0 0     0 if ((eval {require Encode::Base2N;1;} || 0) ne 1) {
221             # if module can't load
222 0         0 require MIME::Base32;
223 0 0       0 $self->{DEBUG} and $self->debug_print("Encode::Base2N unavailable, using MIME::Base32()");
224 0         0 return MIME::Base32::decode_base32(shift);
225             }
226             else {
227             #we have XS!
228 0 0       0 $self->{DEBUG} and $self->debug_print("using Encode::Base2N()");
229 0         0 return Encode::Base2N::decode_base32(shift);
230             }
231             }
232              
233             sub gen_secret {
234 19     19 0 18 my $self = shift;
235 19   50     33 my $length = shift || 20;
236              
237 19         37 my $secret = Crypt::PRNG::random_string_from('/123456789!@#$%^&*()-_+=ABCDIEFGJKLMNPQRSTUVWXYZabcdefghmnopqrstuvwxyz', $length);
238              
239 19 50       3763 if (length($secret) > ($length+1)) {
240 0 0       0 $self->{DEBUG} and $self->debug_print("have len ".length($secret)." ($secret) so cutting down");
241 0         0 return substr($secret,0,$length);
242             }
243 19         42 return $secret;
244             }
245              
246             sub generate_otp {
247 0     0 0 0 my $self = shift;
248 0         0 my ($digits,$period,$algorithm,$secret,$base32secret, $issuer, $user) =
249             $self->process_sub_arguments(\@_,[ 'digits', 'period', 'algorithm', 'secret', 'base32secret', 'issuer', 'user']);
250            
251 0 0       0 unless ($user) {
252 0         0 Carp::confess("need user to use as prefix in generate_otp()");
253             }
254              
255 0         0 $self->valid_digits($digits);
256 0         0 $self->valid_period($period);
257 0         0 $self->valid_algorithm($algorithm);
258 0         0 $self->valid_secret($secret, $base32secret);
259              
260 0 0       0 if ($issuer) {
261 0         0 $issuer = qq[&issuer=].$issuer;
262             }
263             else {
264 0         0 $issuer = '';
265             }
266              
267             return qq[otpauth://totp/$user?secret=]
268             .$self->{base32secret}
269             .qq[&algorithm=].$self->{algorithm}
270             .qq[&digits=].$self->{digits}
271             .qq[&period=].$self->{period}
272 0         0 .$issuer;
273             }
274              
275             sub validate_otp {
276 36     36 0 9415 my $self = shift;
277 36         156 my ($digits,$period,$algorithm,$secret,$when,$tolerance,$base32secret, $otp) =
278             $self->process_sub_arguments(\@_,[ 'digits', 'period', 'algorithm', 'secret', 'when', 'tolerance', 'base32secret', 'otp']);
279            
280 36 50 33     269 unless ($otp && $otp =~ m|^\d{6,8}$|) {
281 0   0     0 $otp ||= "";
282 0         0 Carp::confess("invalid otp $otp passed to validate_otp()");
283             }
284              
285 36         82 $self->valid_digits($digits);
286 36         59 $self->valid_period($period);
287 36         71 $self->valid_algorithm($algorithm);
288 36         68 $self->valid_when($when);
289 36         89 $self->valid_tolerance($tolerance);
290 36         75 $self->valid_secret($secret, $base32secret);
291              
292 36         75 my @tests = ( $self->{when} );
293 36         86 for my $i (1..$self->{tolerance}) {
294 0         0 push @tests, ($self->{when} - ($self->{period} * $i) );
295 0         0 push @tests, ($self->{when} + ($self->{period} * $i) );
296             }
297              
298 36         42 foreach $when (@tests) {
299 36 50       48 $self->{DEBUG} and $self->debug_print("using when $when (". ($when - $self->{when}). ")");
300              
301 36         60 my $code = $self->otp($when);
302              
303 36 50       52 $self->{DEBUG} and $self->debug_print("comparing $code to $otp");
304              
305 36 50       129 if ($code eq sprintf("%0".$self->{digits}."d", $otp) ) {
306 36         130 return 1;
307             }
308              
309             }
310              
311 0         0 return undef;
312             }
313              
314             # Return OTP as digits
315             #
316             # @param ?int $when
317             #
318             # @return string Numeric code as string
319             sub otp {
320 54     54 0 110 my $self = shift;
321 54         60 my $when = shift;
322              
323 54 50       67 if(!defined($when)) {
324 0         0 $when=$self->{when};
325             }
326 54         194 my $T = sprintf("%016x", int($when / $self->{period}) );
327 54         91 my $Td = pack('H*', $T);
328            
329 54         77 my $hmac = $self->hmac($Td);
330            
331             # take the 4 least significant bits (1 hex char) from the encrypted string as an offset
332 54         98 my $offset = hex(substr($hmac, -1));
333             # take the 4 bytes (8 hex chars) at the offset (* 2 for hex), and drop the high bit
334 54         102 my $encrypted = hex(substr($hmac, $offset * 2, 8)) & 0x7fffffff;
335              
336 54         155 my $code = sprintf("%0".$self->{digits}."d", ($encrypted % (10**$self->{digits}) ) );
337              
338 54         94 return $code;
339             }
340              
341              
342             sub initialize {
343 19     19 0 19 my $self = shift;
344              
345 19         36 $self->{DEBUG} = 0;
346              
347 19 100       39 if (@_ != 0) {
348 18 50       38 if (ref $_[0] eq 'HASH') {
    0          
349 18         20 my $hash=$_[0];
350 18         52 foreach (keys %$hash) {
351 90         179 $self->{lc $_}=$hash->{$_};
352             }
353             }
354             elsif (!(scalar(@_)%2)) {
355 0         0 my %hash = @_;
356 0         0 foreach (keys %hash) {
357 0         0 $self->{lc $_}=$hash{$_};
358             }
359             }
360             }
361            
362 19         50 $self->valid_digits();
363 19         39 $self->valid_period();
364 19         31 $self->valid_algorithm();
365 19         37 $self->valid_when();
366 19         39 $self->valid_tolerance();
367 19         29 $self->valid_secret();
368            
369 19         48 return $self;
370             }
371              
372             sub new {
373 19     19 0 156664 my $this = shift;
374 19   33     92 my $class = ref($this) || $this;
375 19         26 my $self = {};
376 19         25 bless $self, $class;
377              
378 19         37 return $self->initialize(@_);
379             }
380              
381             1;
382             __END__