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   409160 use 5.010;
  2         7  
5 2     2   11 use strict;
  2         5  
  2         57  
6 2     2   7 use warnings;
  2         3  
  2         136  
7              
8 2     2   13 use base 'Class::Accessor';
  2         50  
  2         1313  
9              
10 2     2   4665 use Carp 'croak';
  2         4  
  2         126  
11 2     2   974 use Convert::Base32 qw( encode_base32 decode_base32 );
  2         4055  
  2         159  
12 2     2   1082 use Crypt::PRNG 'rand';
  2         10812  
  2         172  
13 2     2   1081 use Digest::HMAC_SHA1 'hmac_sha1_hex';
  2         11119  
  2         122  
14 2     2   1130 use URI::Escape 'uri_escape';
  2         3812  
  2         2153  
15              
16             our $VERSION = '1.09'; # 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 194834 my ($self) = @_;
23 2         24 my @chars = ( 'a' .. 'z', 2 .. 7 );
24 2         8 return $self->secret32( join( '', @chars[ map { rand( scalar(@chars) ) } 1 .. 16 ] ) );
  32         497  
25             }
26              
27             sub clear {
28 1     1 1 5095 my ($self) = @_;
29 1         10 $self->$_(undef) for (@accessors);
30 1         50 return;
31             }
32              
33             sub otpauth {
34 6     6 1 14 my ( $self, $secret32, $key_id, $issuer ) = @_;
35              
36 6         35 $self->_secret_check($secret32);
37              
38 6 100       19 $self->key_id($key_id) if ( defined $key_id );
39 6 100       32 $self->issuer($issuer) if ( defined $issuer );
40              
41 6 100       59 $self->key_id('Undefined') unless ( $self->key_id );
42 6 100       101 $self->issuer('Undefined') unless ( $self->issuer );
43              
44             return
45 6         98 '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 3148 my ( $self, $secret32, $key_id, $issuer, $return_otpauth ) = @_;
52 3         11 my $otpauth = $self->otpauth( $secret32, $key_id, $issuer );
53 3 100       376 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 502 my ( $self, $secret32, $timestamp, $interval ) = @_;
60 25         60 $self->_secret_check($secret32);
61              
62 25   33     51 $timestamp ||= time;
63 25   100     69 $interval ||= 30;
64              
65 25         114 my $hmac = hmac_sha1_hex(
66             pack( 'H*', sprintf( '%016x', int( $timestamp / $interval ) ) ),
67             _decode_base32( $self->secret32 ),
68             );
69              
70 25         750 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 17 my ( $self, $code, $range, $secret32, $timestamp, $interval ) = @_;
78 5         16 $self->_secret_check($secret32);
79              
80 5   50     16 $code ||= '';
81 5   100     18 $range ||= 0;
82 5   33     8 $timestamp ||= time;
83 5   50     23 $interval ||= 30;
84              
85 5 50 33     36 croak('Range value not zero or a positive number') unless ( $range =~ /^\d+$/ and $range >= 0 );
86              
87 5         16 for ( 0 .. $range ) {
88 9 100 100     37 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         12 return 0;
98             }
99              
100             sub _secret_check {
101 36     36   75 my ( $self, $secret32 ) = @_;
102              
103 36 100       83 if ($secret32) {
104 30         105 $self->secret32($secret32);
105 30         331 $self->secret( _decode_base32($secret32) );
106             }
107              
108 36 100       323 if ( not $self->secret32 ) {
109 1 50       17 if ( not $self->secret ) {
110 1         24 $self->secret( _decode_base32( $self->generate_secret32 ) );
111             }
112             else {
113 0         0 $self->secret32( encode_base32( $self->secret ) );
114             }
115             }
116              
117 36         451 return;
118             }
119              
120             sub _decode_base32 {
121 56     56   319 my ($data) = @_;
122 56         68 my $rv;
123 56         79 eval{ $rv = decode_base32($data) };
  56         115  
124 56 50       1459 croak("Error decoding what should be base32 data: $data")
125             if ( $@ =~ /Data contains non-base32 characters/ );
126 56         168 return $rv;
127             }
128              
129             1;
130              
131             __END__