File Coverage

blib/lib/Authen/TOTP.pm
Criterion Covered Total %
statement 126 176 71.5
branch 38 80 47.5
condition 22 47 46.8
subroutine 20 24 83.3
pod 2 18 11.1
total 208 345 60.2


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