File Coverage

blib/lib/Auth/GoogleAuth.pm
Criterion Covered Total %
statement 71 72 98.6
branch 19 22 86.3
condition 17 26 65.3
subroutine 16 16 100.0
pod 5 5 100.0
total 128 141 90.7


line stmt bran cond sub pod time code
1             package Auth::GoogleAuth;
2             # ABSTRACT: Google Authenticator TBOT Abstraction
3              
4 1     1   93808 use 5.008;
  1         11  
5 1     1   4 use strict;
  1         2  
  1         15  
6 1     1   3 use warnings;
  1         2  
  1         28  
7              
8 1     1   4 use base 'Class::Accessor';
  1         2  
  1         389  
9              
10 1     1   1822 use Digest::HMAC_SHA1 'hmac_sha1_hex';
  1         3414  
  1         41  
11 1     1   419 use Math::Random::MT 'rand';
  1         831  
  1         3  
12 1     1   406 use URI::Escape 'uri_escape';
  1         1449  
  1         47  
13 1     1   336 use Convert::Base32 qw( encode_base32 decode_base32 );
  1         1184  
  1         65  
14 1     1   6 use Carp 'croak';
  1         1  
  1         704  
15              
16             our $VERSION = '1.02'; # VERSION
17              
18             my @accessors = qw( secret secret32 issuer key_id otpauth );
19             __PACKAGE__->mk_accessors(@accessors);
20              
21             sub generate_secret32 {
22 2     2 1 1200 my ($self) = @_;
23 2         13 my @chars = ( 'a' .. 'z', 2 .. 7 );
24 2         5 return $self->secret32( join( '', @chars[ map { rand( scalar(@chars) ) } 1 .. 16 ] ) );
  32         317  
25             }
26              
27             sub clear {
28 1     1 1 2778 my ($self) = @_;
29 1         13 $self->$_(undef) for (@accessors);
30 1         46 return;
31             }
32              
33             sub qr_code {
34 3     3 1 1991 my ( $self, $secret32, $key_id, $issuer, $return_otpauth ) = @_;
35 3         8 $self->_secret_check($secret32);
36              
37 3 100       6 $self->key_id($key_id) if ($key_id);
38 3 100       22 $self->issuer($issuer) if ($issuer);
39              
40 3 100       16 $self->key_id('Undefined') unless ( $self->key_id );
41 3 100       35 $self->issuer('Undefined') unless ( $self->issuer );
42              
43 3         34 $self->otpauth(
44             'otpauth://totp/' .
45             uri_escape( $self->issuer ) . ':' . uri_escape( $self->key_id ) .
46             '?secret=' . $self->secret32 . '&issuer=' . uri_escape( $self->issuer )
47             );
48              
49 3 100       247 return ($return_otpauth)
50             ? $self->otpauth
51             : 'https://chart.googleapis.com/chart?chs=200x200&cht=qr&chl=' . uri_escape( $self->otpauth );
52             }
53              
54             sub code {
55 25     25 1 287 my ( $self, $secret32, $timestamp, $interval ) = @_;
56 25         50 $self->_secret_check($secret32);
57              
58 25   33     34 $timestamp ||= time;
59 25   100     45 $interval ||= 30;
60              
61 25         95 my $hmac = hmac_sha1_hex(
62             pack( 'H*', sprintf( '%016x', int( $timestamp / $interval ) ) ),
63             _decode_base32( $self->secret32 ),
64             );
65              
66 25         541 return sprintf(
67             '%06d',
68             ( hex( substr( $hmac, hex( substr( $hmac, -1 ) ) * 2, 8 ) ) & 0x7fffffff ) % 1000000
69             );
70             }
71              
72             sub verify {
73 5     5 1 12 my ( $self, $code, $range, $secret32, $timestamp, $interval ) = @_;
74 5         13 $self->_secret_check($secret32);
75              
76 5   50     10 $code ||= '';
77 5   100     14 $range ||= 0;
78 5   33     7 $timestamp ||= time;
79 5   50     19 $interval ||= 30;
80              
81 5 50 33     25 croak('Range value not zero or a positive number') unless ( $range =~ /^\d+$/ and $range >= 0 );
82              
83 5         12 for ( 0 .. $range ) {
84 9 100 100     26 return 1 if (
      66        
      100        
85             not $_ and $code eq $self->code( $secret32, $timestamp, $interval )
86             or
87             $code eq $self->code( $secret32, $timestamp + $interval * $_, $interval )
88             or
89             $code eq $self->code( $secret32, $timestamp - $interval * $_, $interval )
90             );
91             }
92              
93 2         7 return 0;
94             }
95              
96             sub _secret_check {
97 33     33   42 my ( $self, $secret32 ) = @_;
98              
99 33 100       57 if ($secret32) {
100 30         60 $self->secret32($secret32);
101 30         266 $self->secret( _decode_base32($secret32) );
102             }
103              
104 33 100       259 if ( not $self->secret32 ) {
105 1 50       12 if ( not $self->secret ) {
106 1         11 $self->secret( _decode_base32( $self->generate_secret32 ) );
107             }
108             else {
109 0         0 $self->secret32( encode_base32( $self->secret ) );
110             }
111             }
112              
113 33         272 return;
114             }
115              
116             sub _decode_base32 {
117 56     56   256 my ($data) = @_;
118 56         61 my $rv;
119 56         61 eval{ $rv = decode_base32($data) };
  56         85  
120 56 50       1711 croak("Error decoding what should be base32 data: $data")
121             if ( $@ =~ /Data contains non-base32 characters/ );
122 56         125 return $rv;
123             }
124              
125             1;
126              
127             __END__