File Coverage

blib/lib/Auth/GoogleAuth.pm
Criterion Covered Total %
statement 73 74 98.6
branch 19 22 86.3
condition 17 26 65.3
subroutine 17 17 100.0
pod 6 6 100.0
total 132 145 91.0


line stmt bran cond sub pod time code
1             package Auth::GoogleAuth;
2             # ABSTRACT: Google Authenticator TBOT Abstraction
3              
4 2     2   362848 use 5.010;
  2         9  
5 2     2   10 use strict;
  2         4  
  2         69  
6 2     2   8 use warnings;
  2         3  
  2         111  
7              
8 2     2   9 use base 'Class::Accessor';
  2         4  
  2         1270  
9              
10 2     2   4129 use Carp 'croak';
  2         4  
  2         142  
11 2     2   1004 use Convert::Base32 qw( encode_base32 decode_base32 );
  2         4727  
  2         198  
12 2     2   1310 use Crypt::PRNG 'rand';
  2         11429  
  2         195  
13 2     2   1270 use Digest::HMAC_SHA1 'hmac_sha1_hex';
  2         13349  
  2         150  
14 2     2   1163 use URI::Escape 'uri_escape';
  2         4630  
  2         2502  
15              
16             our $VERSION = '1.10'; # VERSION
17              
18             my @accessors = qw( secret secret32 issuer key_id );
19             __PACKAGE__->mk_accessors(@accessors);
20              
21             sub generate_secret32 {
22 2     2 1 261077 my ($self) = @_;
23 2         25 my @chars = ( 'a' .. 'z', 2 .. 7 );
24 2         9 return $self->secret32( join( '', @chars[ map { rand( scalar(@chars) ) } 1 .. 16 ] ) );
  32         473  
25             }
26              
27             sub clear {
28 1     1 1 3498 my ($self) = @_;
29 1         10 $self->$_(undef) for (@accessors);
30 1         50 return;
31             }
32              
33             sub otpauth {
34 6     6 1 16 my ( $self, $secret32, $key_id, $issuer ) = @_;
35              
36 6         21 $self->_secret_check($secret32);
37              
38 6 100       19 $self->key_id($key_id) if ( defined $key_id );
39 6 100       37 $self->issuer($issuer) if ( defined $issuer );
40              
41 6 100       33 $self->key_id('Undefined') unless ( $self->key_id );
42 6 100       82 $self->issuer('Undefined') unless ( $self->issuer );
43              
44             return
45 6         77 'otpauth://totp/' .
46             uri_escape( $self->issuer ) . ':' . uri_escape( $self->key_id ) .
47             '?secret=' . $self->secret32 . '&issuer=' . uri_escape( $self->issuer );
48             }
49              
50             sub qr_code {
51 3     3 1 3388 my ( $self, $secret32, $key_id, $issuer, $return_otpauth ) = @_;
52 3         10 my $otpauth = $self->otpauth( $secret32, $key_id, $issuer );
53 3 100       373 return ($return_otpauth)
54             ? $self->otpauth
55             : 'https://quickchart.io/chart?chs=200x200&cht=qr&chl=' . uri_escape( $self->otpauth );
56             }
57              
58             sub code {
59 25     25 1 529 my ( $self, $secret32, $timestamp, $interval ) = @_;
60 25         99 $self->_secret_check($secret32);
61              
62 25   33     62 $timestamp ||= time;
63 25   100     62 $interval ||= 30;
64              
65 25         187 my $hmac = hmac_sha1_hex(
66             pack( 'H*', sprintf( '%016x', int( $timestamp / $interval ) ) ),
67             _decode_base32( $self->secret32 ),
68             );
69              
70 25         956 return sprintf(
71             '%06d',
72             ( hex( substr( $hmac, hex( substr( $hmac, -1 ) ) * 2, 8 ) ) & 0x7fffffff ) % 1000000
73             );
74             }
75              
76             sub verify {
77 5     5 1 24 my ( $self, $code, $range, $secret32, $timestamp, $interval ) = @_;
78 5         16 $self->_secret_check($secret32);
79              
80 5   50     14 $code ||= '';
81 5   100     22 $range ||= 0;
82 5   33     11 $timestamp ||= time;
83 5   50     26 $interval ||= 30;
84              
85 5 50 33     43 croak('Range value not zero or a positive number') unless ( $range =~ /^\d+$/ and $range >= 0 );
86              
87 5         18 for ( 0 .. $range ) {
88 9 100 100     58 return 1 if (
      66        
      100        
89             not $_ and $code eq $self->code( $secret32, $timestamp, $interval )
90             or
91             $code eq $self->code( $secret32, $timestamp + $interval * $_, $interval )
92             or
93             $code eq $self->code( $secret32, $timestamp - $interval * $_, $interval )
94             );
95             }
96              
97 2         17 return 0;
98             }
99              
100             sub _secret_check {
101 36     36   83 my ( $self, $secret32 ) = @_;
102              
103 36 100       138 if ($secret32) {
104 30         103 $self->secret32($secret32);
105 30         459 $self->secret( _decode_base32($secret32) );
106             }
107              
108 36 100       445 if ( not $self->secret32 ) {
109 1 50       17 if ( not $self->secret ) {
110 1         49 $self->secret( _decode_base32( $self->generate_secret32 ) );
111             }
112             else {
113 0         0 $self->secret32( encode_base32( $self->secret ) );
114             }
115             }
116              
117 36         501 return;
118             }
119              
120             sub _decode_base32 {
121 56     56   407 my ($data) = @_;
122 56         78 my $rv;
123 56         98 eval{ $rv = decode_base32($data) };
  56         150  
124 56 50       1831 croak("Error decoding what should be base32 data: $data")
125             if ( $@ =~ /Data contains non-base32 characters/ );
126 56         247 return $rv;
127             }
128              
129             1;
130              
131             __END__