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 3     3   709100 use utf8;
  3         5  
  3         19  
20 3     3   117 use strict;
  3         5  
  3         61  
21 3     3   17 use warnings;
  3         5  
  3         184  
22              
23 3     3   1519 use MIME::Base32 qw(decode_base32);
  3         3894  
  3         248  
24 3     3   1474 use Digest::HMAC;
  3         1941  
  3         164  
25 3     3   1706 use Digest::SHA;
  3         11265  
  3         235  
26 3     3   7673 use Math::BigInt;
  3         143593  
  3         18  
27              
28             require Exporter;
29             our @ISA = qw(Exporter);
30             our @EXPORT_OK = qw(otp hotp totp);
31              
32             our $VERSION = '1.900';
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 24     24 1 116 my %options = (
65             algorithm => 'sha1',
66             counter => 0,
67             digits => 6,
68             @_,
69             );
70              
71 24         84 my $C = Math::BigInt->new($options{counter});
72              
73 24         1882 my ($hex) = $C->as_hex =~ /^0x(.*)/;
74 24         1746 $hex = '0' x (16 - length($hex)) . $hex;
75              
76 24         111 my ($algorithm) = $options{algorithm} =~ /sha(\d+)/i;
77 24         102 my $digest = Digest::SHA->new($algorithm);
78             my $hmac = Digest::HMAC->new(
79 24 100       509 $options{base32} ? decode_base32($options{secret} =~ s/ //gr) : pack('H*', $options{secret}),
    100          
80             $digest,
81             $algorithm < 384 ? 64 : 128,
82             );
83 24         766 $hmac->add(pack 'H*', $hex);
84 24         140 my $hash = $hmac->digest;
85              
86 24         569 my $offset = hex(substr(unpack('H*', $hash), -1));
87 24         65 my $bin_code = unpack('N', substr($hash, $offset, 4));
88 24         38 $bin_code &= 0x7fffffff;
89 24         53 $bin_code = Math::BigInt->new($bin_code);
90              
91 24 100       2121 if (defined $options{chars}) {
92 1         4 my $otp = '';
93 1         4 foreach (1 .. $options{digits}) {
94 5         1797 $otp .= substr($options{chars}, $bin_code->copy->bmod(length($options{chars})), 1);
95 5         2170 $bin_code = $bin_code->btdiv(length($options{chars}));
96             }
97 1         468 return $otp;
98             }
99             else {
100 23         102 my $otp = $bin_code->bmod(10**$options{digits});
101 23         5253 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 79 my %options = (
116             'start-time' => 0,
117             now => time,
118             period => 30,
119             @_,
120             );
121              
122 20         103 $options{counter} = Math::BigInt->new(int(($options{now} - $options{'start-time'}) / $options{period}));
123 20         1689 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 24     24 1 190893 my %options = (
134             type => 'hotp',
135             @_,
136             );
137              
138             return totp(
139             %options,
140             digits => 5,
141             chars => '23456789BCDFGHJKMNPQRTVWXY',
142 24 100 66     100 ) if defined $options{issuer} and $options{issuer} =~ /^Steam/i;
143              
144 23 100       70 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;