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   581561 use utf8;
  2         5  
  2         17  
20 2     2   120 use strict;
  2         5  
  2         68  
21 2     2   28 use warnings;
  2         10  
  2         168  
22              
23 2     2   1259 use MIME::Base32 qw(decode_base32);
  2         3155  
  2         194  
24 2     2   1255 use Digest::HMAC;
  2         1588  
  2         151  
25 2     2   1252 use Digest::SHA;
  2         8852  
  2         191  
26 2     2   3431 use Math::BigInt;
  2         121281  
  2         34  
27              
28             require Exporter;
29             our @ISA = qw(Exporter);
30             our @EXPORT_OK = qw(otp hotp totp);
31              
32             our $VERSION = '1.801';
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 23     23 1 194 my %options = (
65             algorithm => 'sha1',
66             counter => 0,
67             digits => 6,
68             @_,
69             );
70              
71 23         94 my $C = Math::BigInt->new($options{counter});
72              
73 23         2176 my ($hex) = $C->as_hex =~ /^0x(.*)/;
74 23         2275 $hex = "0" x (16 - length($hex)) . $hex;
75              
76 23         139 my ($algorithm) = $options{algorithm} =~ /sha(\d+)/i;
77 23         99 my $digest = Digest::SHA->new($algorithm);
78             my $hmac = Digest::HMAC->new(
79 23 100       587 $options{base32} ? decode_base32($options{secret} =~ s/ //gr) : pack('H*', $options{secret}),
    100          
80             $digest,
81             $algorithm < 384? 64 : 128,
82             );
83 23         1039 $hmac->add(pack 'H*', $hex);
84 23         190 my $hash = $hmac->digest;
85              
86 23         692 my $offset = hex(substr(unpack('H*', $hash), -1));
87 23         77 my $bin_code = unpack('N', substr($hash, $offset, 4));
88 23         44 $bin_code &= 0x7fffffff;
89 23         80 $bin_code = Math::BigInt->new($bin_code);
90              
91 23 100       2887 if (defined $options{chars}) {
92 1         3 my $otp = "";
93 1         4 foreach (1 .. $options{digits}) {
94 5         1761 $otp .= substr($options{chars}, $bin_code->copy->bmod(length($options{chars})), 1);
95 5         1925 $bin_code = $bin_code->btdiv(length($options{chars}));
96             }
97 1         415 return $otp;
98             }
99             else {
100 22         186 my $otp = $bin_code->bmod(10**$options{digits});
101 22         7504 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 20     20 1 95 my %options = (
116             'start-time' => 0,
117             now => time,
118             period => 30,
119             @_,
120             );
121              
122 20         142 $options{counter} = Math::BigInt->new(int(($options{now} - $options{'start-time'}) / $options{period}));
123 20         2510 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 23     23 1 47558 my %options = (
134             type => 'hotp',
135             @_,
136             );
137              
138             return totp(
139             %options,
140             digits => 5,
141             chars => "23456789BCDFGHJKMNPQRTVWXY",
142 23 100 66     111 ) if defined $options{issuer} and $options{issuer} =~ /^Steam/i;
143              
144 22 100       82 return hotp(%options) if $options{type} eq 'hotp';
145 19 50       83 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;