File Coverage

blib/lib/Pass/OTP.pm
Criterion Covered Total %
statement 47 48 97.9
branch 7 10 70.0
condition 2 3 66.6
subroutine 10 10 100.0
pod 3 3 100.0
total 69 74 93.2


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 1     1   726 use utf8;
  1         2  
  1         9  
20 1     1   36 use strict;
  1         2  
  1         28  
21 1     1   5 use warnings;
  1         1  
  1         35  
22              
23 1     1   537 use Convert::Base32 qw(decode_base32);
  1         2369  
  1         67  
24 1     1   456 use Digest::HMAC;
  1         594  
  1         47  
25 1     1   564 use Digest::SHA;
  1         3388  
  1         51  
26 1     1   1220 use Math::BigInt;
  1         27037  
  1         6  
27              
28             require Exporter;
29             our @ISA = qw(Exporter);
30             our @EXPORT_OK = qw(otp hotp totp);
31              
32             our $VERSION = '1.4';
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 4     4 1 19 my %options = (
65             algorithm => 'sha1',
66             counter => 0,
67             digits => 6,
68             @_,
69             );
70              
71 4         19 my $C = Math::BigInt->new($options{counter});
72              
73 4         405 my ($hex) = $C->as_hex =~ /^0x(.*)/;
74 4         138 $hex = "0" x (16 - length($hex)) . $hex;
75              
76 4         28 my $digest = Digest::SHA->new($options{algorithm} =~ /sha(\d+)/);
77             my $hmac = Digest::HMAC->new(
78 4 100       128 $options{base32} ? decode_base32($options{secret} =~ s/ //gr) : pack('H*', $options{secret}),
79             $digest,
80             );
81 4         1168 $hmac->add(pack 'H*', $hex);
82 4         29 my $hash = $hmac->digest;
83              
84 4         102 my $offset = hex(substr(unpack('H*', $hash), -1));
85 4         13 my $bin_code = unpack('N', substr($hash, $offset, 4));
86 4         9 $bin_code &= 0x7fffffff;
87 4         12 $bin_code = Math::BigInt->new($bin_code);
88              
89 4 100       220 if (defined $options{chars}) {
90 1         2 my $otp = "";
91 1         4 foreach (1 .. $options{digits}) {
92 5         883 $otp .= substr($options{chars}, $bin_code->copy->bmod(length($options{chars})), 1);
93 5         985 $bin_code = $bin_code->btdiv(length($options{chars}));
94             }
95 1         196 return $otp;
96             }
97             else {
98 3         11 my $otp = $bin_code->bmod(10**$options{digits});
99 3         578 return "0" x ($options{digits} - length($otp)) . $otp;
100             }
101             }
102              
103             =item totp(%options)
104              
105             Computes Time-based One-time Password (RFC 6238).
106              
107             TOTP = HOTP(K,T)
108             T = (Current Unix time - T0) / X
109              
110             =cut
111              
112             sub totp {
113 1     1 1 7 my %options = (
114             'start-time' => 0,
115             now => time,
116             period => 30,
117             @_,
118             );
119              
120 1         8 $options{counter} = Math::BigInt->new(int(($options{now} - $options{'start-time'}) / $options{period}));
121 1         155 return hotp(%options);
122             }
123              
124             =item otp(%options)
125              
126             Convenience wrapper which calls totp/hotp according to options.
127              
128             =cut
129              
130             sub otp {
131 4     4 1 1034 my %options = (
132             type => 'hotp',
133             @_,
134             );
135              
136             return totp(
137             %options,
138             digits => 5,
139             chars => "23456789BCDFGHJKMNPQRTVWXY",
140 4 100 66     25 ) if defined $options{issuer} and $options{issuer} =~ /^Steam/i;
141              
142 3 50       15 return hotp(%options) if $options{type} eq 'hotp';
143 0 0         return totp(%options) if $options{type} eq 'totp';
144             }
145              
146             =back
147              
148             =head1 SEE ALSO
149              
150             L
151              
152             L
153              
154             RFC 4226
155             RFC 6238
156              
157             L
158              
159             =head1 COPYRIGHT AND LICENSE
160              
161             Copyright (C) 2020 Jan Baier
162              
163             This program is free software; you can redistribute it and/or modify it
164             under the terms of either: the GNU General Public License as published
165             by the Free Software Foundation; or the Artistic License.
166              
167             See L for more information.
168              
169             =cut
170              
171             1;