File Coverage

blib/lib/Pass/OTP.pm
Criterion Covered Total %
statement 49 49 100.0
branch 11 12 91.6
condition 2 3 66.6
subroutine 10 10 100.0
pod 3 3 100.0
total 75 77 97.4


line stmt bran cond sub pod time code
1             package Pass::OTP;
2              
3             =encoding utf8
4              
5             =head1 NAME
6              
7             Pass::OTP - Perl implementation of HOTP / TOTP algorithms
8              
9             =head1 SYNOPSIS
10              
11             use Pass::OTP qw(otp);
12             use Pass::OTP::URI qw(parse);
13              
14             my $uri = "otpauth://totp/ACME:john.doe@email.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME&digits=6";
15             my $otp_code = otp(parse($uri));
16              
17             =cut
18              
19 2     2   1300 use utf8;
  2         4  
  2         10  
20 2     2   66 use strict;
  2         4  
  2         36  
21 2     2   9 use warnings;
  2         4  
  2         71  
22              
23 2     2   891 use Convert::Base32 qw(decode_base32);
  2         4426  
  2         119  
24 2     2   871 use Digest::HMAC;
  2         1157  
  2         93  
25 2     2   1052 use Digest::SHA;
  2         6201  
  2         96  
26 2     2   2425 use Math::BigInt;
  2         53684  
  2         10  
27              
28             require Exporter;
29             our @ISA = qw(Exporter);
30             our @EXPORT_OK = qw(otp hotp totp);
31              
32             our $VERSION = '1.6';
33              
34             =head1 DESCRIPTION
35              
36             The C module provides implementation of HOTP and TOTP algorithms according to the RFC 4226 and RFC 6238.
37              
38             =head1 FUNCTIONS
39              
40             =over 4
41              
42             =item hotp(%options)
43              
44             Computes HMAC-based One-time Password (RFC 4226).
45              
46             HOTP(K,C) = Truncate(HMAC-SHA-1(K,C))
47              
48             Step 1: Generate an HMAC-SHA-1 value
49              
50             Let HS = HMAC-SHA-1(K,C)
51              
52             Step 2: Generate a 4-byte string (Dynamic Truncation)
53              
54             Let Sbits = DT(HS)
55              
56             Step 3: Compute an HOTP value
57              
58             Let Snum = StToNum(Sbits) # Convert S to a number in 0..2^{31}-1
59             Return D = Snum mod 10^Digit # D us a number in the range 0..10^{Digit}-1
60              
61             =cut
62              
63             sub hotp {
64 22     22 1 92 my %options = (
65             algorithm => 'sha1',
66             counter => 0,
67             digits => 6,
68             @_,
69             );
70              
71 22         77 my $C = Math::BigInt->new($options{counter});
72              
73 22         1094 my ($hex) = $C->as_hex =~ /^0x(.*)/;
74 22         539 $hex = "0" x (16 - length($hex)) . $hex;
75              
76 22         105 my ($algorithm) = $options{algorithm} =~ /sha(\d+)/;
77 22         85 my $digest = Digest::SHA->new($algorithm);
78             my $hmac = Digest::HMAC->new(
79 22 100       421 $options{base32} ? decode_base32($options{secret} =~ s/ //gr) : pack('H*', $options{secret}),
    100          
80             $digest,
81             $algorithm < 384? 64 : 128,
82             );
83 22         1639 $hmac->add(pack 'H*', $hex);
84 22         150 my $hash = $hmac->digest;
85              
86 22         558 my $offset = hex(substr(unpack('H*', $hash), -1));
87 22         59 my $bin_code = unpack('N', substr($hash, $offset, 4));
88 22         39 $bin_code &= 0x7fffffff;
89 22         50 $bin_code = Math::BigInt->new($bin_code);
90              
91 22 100       1073 if (defined $options{chars}) {
92 1         2 my $otp = "";
93 1         3 foreach (1 .. $options{digits}) {
94 5         788 $otp .= substr($options{chars}, $bin_code->copy->bmod(length($options{chars})), 1);
95 5         1011 $bin_code = $bin_code->btdiv(length($options{chars}));
96             }
97 1         189 return $otp;
98             }
99             else {
100 21         71 my $otp = $bin_code->bmod(10**$options{digits});
101 21         3561 return "0" x ($options{digits} - length($otp)) . $otp;
102             }
103             }
104              
105             =item totp(%options)
106              
107             Computes Time-based One-time Password (RFC 6238).
108              
109             TOTP = HOTP(K,T)
110             T = (Current Unix time - T0) / X
111              
112             =cut
113              
114             sub totp {
115 19     19 1 74 my %options = (
116             'start-time' => 0,
117             now => time,
118             period => 30,
119             @_,
120             );
121              
122 19         99 $options{counter} = Math::BigInt->new(int(($options{now} - $options{'start-time'}) / $options{period}));
123 19         991 return hotp(%options);
124             }
125              
126             =item otp(%options)
127              
128             Convenience wrapper which calls totp/hotp according to options.
129              
130             =cut
131              
132             sub otp {
133 22     22 1 11979 my %options = (
134             type => 'hotp',
135             @_,
136             );
137              
138             return totp(
139             %options,
140             digits => 5,
141             chars => "23456789BCDFGHJKMNPQRTVWXY",
142 22 100 66     80 ) if defined $options{issuer} and $options{issuer} =~ /^Steam/i;
143              
144 21 100       56 return hotp(%options) if $options{type} eq 'hotp';
145 18 50       66 return totp(%options) if $options{type} eq 'totp';
146             }
147              
148             =back
149              
150             =head1 SEE ALSO
151              
152             L
153              
154             L
155              
156             RFC 4226
157             RFC 6238
158              
159             L
160              
161             =head1 COPYRIGHT AND LICENSE
162              
163             Copyright (C) 2020 Jan Baier
164              
165             This program is free software; you can redistribute it and/or modify it
166             under the terms of either: the GNU General Public License as published
167             by the Free Software Foundation; or the Artistic License.
168              
169             See L for more information.
170              
171             =cut
172              
173             1;