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   173278 use 5.008;
  1         7  
5 1     1   4 use strict;
  1         2  
  1         14  
6 1     1   3 use warnings;
  1         2  
  1         19  
7              
8 1     1   3 use base 'Class::Accessor';
  1         2  
  1         375  
9              
10 1     1   1692 use Digest::HMAC_SHA1 'hmac_sha1_hex';
  1         3781  
  1         42  
11 1     1   332 use Math::Random::MT 'rand';
  1         729  
  1         4  
12 1     1   340 use URI::Escape 'uri_escape';
  1         1130  
  1         45  
13 1     1   312 use Convert::Base32 qw( encode_base32 decode_base32 );
  1         1057  
  1         46  
14 1     1   5 use Carp 'croak';
  1         1  
  1         637  
15              
16             our $VERSION = '1.04'; # 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 12731 my ($self) = @_;
23 2         28 my @chars = ( 'a' .. 'z', 2 .. 7 );
24 2         7 return $self->secret32( join( '', @chars[ map { rand( scalar(@chars) ) } 1 .. 16 ] ) );
  32         268  
25             }
26              
27             sub clear {
28 1     1 1 2215 my ($self) = @_;
29 1         8 $self->$_(undef) for (@accessors);
30 1         50 return;
31             }
32              
33             sub qr_code {
34 3     3 1 1826 my ( $self, $secret32, $key_id, $issuer, $return_otpauth ) = @_;
35 3         9 $self->_secret_check($secret32);
36              
37 3 100       7 $self->key_id($key_id) if ($key_id);
38 3 100       19 $self->issuer($issuer) if ($issuer);
39              
40 3 100       15 $self->key_id('Undefined') unless ( $self->key_id );
41 3 100       35 $self->issuer('Undefined') unless ( $self->issuer );
42              
43 3         31 $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       252 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 236 my ( $self, $secret32, $timestamp, $interval ) = @_;
56 25         37 $self->_secret_check($secret32);
57              
58 25   33     32 $timestamp ||= time;
59 25   100     42 $interval ||= 30;
60              
61 25         90 my $hmac = hmac_sha1_hex(
62             pack( 'H*', sprintf( '%016x', int( $timestamp / $interval ) ) ),
63             _decode_base32( $self->secret32 ),
64             );
65              
66 25         491 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 13 my ( $self, $code, $range, $secret32, $timestamp, $interval ) = @_;
74 5         10 $self->_secret_check($secret32);
75              
76 5   50     11 $code ||= '';
77 5   100     14 $range ||= 0;
78 5   33     6 $timestamp ||= time;
79 5   50     17 $interval ||= 30;
80              
81 5 50 33     30 croak('Range value not zero or a positive number') unless ( $range =~ /^\d+$/ and $range >= 0 );
82              
83 5         10 for ( 0 .. $range ) {
84 9 100 100     24 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         8 return 0;
94             }
95              
96             sub _secret_check {
97 33     33   39 my ( $self, $secret32 ) = @_;
98              
99 33 100       108 if ($secret32) {
100 30         57 $self->secret32($secret32);
101 30         239 $self->secret( _decode_base32($secret32) );
102             }
103              
104 33 100       239 if ( not $self->secret32 ) {
105 1 50       22 if ( not $self->secret ) {
106 1         12 $self->secret( _decode_base32( $self->generate_secret32 ) );
107             }
108             else {
109 0         0 $self->secret32( encode_base32( $self->secret ) );
110             }
111             }
112              
113 33         242 return;
114             }
115              
116             sub _decode_base32 {
117 56     56   233 my ($data) = @_;
118 56         53 my $rv;
119 56         54 eval{ $rv = decode_base32($data) };
  56         76  
120 56 50       2289 croak("Error decoding what should be base32 data: $data")
121             if ( $@ =~ /Data contains non-base32 characters/ );
122 56         112 return $rv;
123             }
124              
125             1;
126              
127             __END__