File Coverage

blib/lib/Authen/TOTP.pm
Criterion Covered Total %
statement 114 161 70.8
branch 30 66 45.4
condition 20 45 44.4
subroutine 19 23 82.6
pod 1 16 6.2
total 184 311 59.1


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