File Coverage

blib/lib/Authen/TOTP.pm
Criterion Covered Total %
statement 129 184 70.1
branch 41 86 47.6
condition 23 49 46.9
subroutine 20 25 80.0
pod 3 19 15.7
total 216 363 59.5


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