File Coverage

blib/lib/Authen/OATH/OCRA.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1             #!/usr/bin/perl
2             package Authen::OATH::OCRA;
3 1     1   19617 use warnings;
  1         2  
  1         27  
4 1     1   4 use strict;
  1         2  
  1         28  
5 1     1   1532 use Math::BigInt;
  1         26354  
  1         6  
6 1     1   24052 use Moose;
  0            
  0            
7             use Carp;
8             use Digest::SHA qw(hmac_sha1 hmac_sha256 hmac_sha512);
9              
10             has 'ocrasuite' => (
11             'is' => 'rw',
12             'isa' => 'Str',
13             );
14              
15             has 'key' => (
16             'is' => 'rw',
17             'isa' => 'Str',
18             );
19              
20             has 'counter' => (
21             'is' => 'rw',
22             'isa' => 'Int'
23             );
24              
25             has 'question' => (
26             'is' => 'rw',
27             'isa' => 'Str'
28             );
29              
30             has 'password' => (
31             'is' => 'rw',
32             'isa' => 'Str'
33             );
34              
35             has 'session_information' => (
36             'is' => 'rw',
37             'isa' => 'Str'
38             );
39              
40             has 'timestamp' => (
41             'is' => 'rw',
42             'isa' => 'Int'
43             );
44              
45             =head1 NAME
46              
47             OCRA - OATH Challenge-Response Algorithm
48              
49             =head1 VERSION
50              
51             Version 1.01
52              
53             =cut
54              
55             our $VERSION = "1.01";
56              
57             =head1 SYNOPSIS
58              
59             use Authen::OATH::OCRA;
60             my $key = '7110eda4d09e062aa5e4a390b0a572ac0d2c0220';
61             my $question = 'This is the challenge';
62             my $ocrasuite = 'OCRA-1:HOTP-SHA1-6:QA32';
63             my $ocra = Authen::OATH::OCRA->new(
64             ocrasuite => $ocrasuite,
65             key => $key, # key must be hex encoded
66             question => $question
67             );
68             my $otp = $ocra->ocra();
69              
70             Parameters may be set after object instantiation using accesor methods before the ocra() method is called
71              
72             use Authen::OATH::OCRA;
73             my $ocra = Authen::OATH::OCRA->new();
74             $ocra->ocrasuite('OCRA-1:HOTP-SHA512-6:C-QA32-PSHA1-S20-T1M');
75             $ocra->key('7110eda4d09e062aa5e4a390b0a572ac0d2c0220');
76             $ocra->counter(77777777);
77             $ocra->question("I bet you can't");
78             $ocra->password('f7c3bc1d808e04732adf679965ccc34ca7ae3441');
79             $ocra->session_information('Some session info');
80             $ocra->timestamp(1234567890);
81             my $otp = $ocra->ocra();
82              
83             =head1 Description
84              
85             Implementation of the OATH Challenge-Response authentication algorithm
86             as defined by The Initiative for Open Authentication OATH (http://www.openauthentication.org)
87             in RFC 6287 (http://tools.ietf.org/html/rfc6287)
88              
89              
90              
91             =head1 PARAMETERS
92              
93             Minimum required parameters are: ocrasuite, key and question.
94             Aditional parameters (counter, password or session_information) may be required depending on the specified ocrasuite.
95              
96             Accesor methods are provided for each parameter
97              
98             =head2 ocrasuite
99              
100             Text string that specifies the operation mode for OCRA. For further information see http://tools.ietf.org/html/rfc6287#section-6
101              
102             =head2 key
103              
104             Text string with the shared secret key known to both parties, must be in hexadecimal format
105              
106             =head2 counter
107              
108             An unsigned integer value, must be sinchronized between both parties
109              
110             =head2 question
111              
112             Text string with the challenge question
113              
114             =head2 password
115              
116             Text string with the hash (SHA-1 , SHA-256 and SHA-512 are supported) value of PIN/password that is known to both parties, must be in hexadecimal format
117              
118             =head2 session_information
119              
120             Text string that contains information about the current session, must be UTF-8 encoded
121              
122             =head2 timestamp
123              
124             Defaults to system time if required by the OCRA Suite and not provided, use only if you need to set the time manually. An unsigned integer value representing the manual Unix Time in the granularity specified in the OCRA Suite
125              
126             =head1 SUBROUTINES/METHODS
127              
128             =head2 ocra
129              
130             Returns a text string with the One Time Password for the provided parameters
131              
132             my $otp = $ocra->ocra();
133              
134             ocra() passed all the test vectors contained in the RFC document.
135             =cut
136              
137             sub ocra {
138             my ($self) = @_;
139              
140             #Validate that min required parameters are present
141             croak "Parameter \"ocrasuite\" is required"
142             unless defined( $self->{ocrasuite} );
143             croak "Parameter \"question\" is required"
144             unless defined( $self->{question} );
145             croak "Parameter \"key\" is required" unless defined( $self->{key} );
146              
147             #Validate the OCRA Suite format and parse sub parameters into variables
148             croak "Invalid ocrasuite"
149             unless $self->{ocrasuite} =~ /^
150             OCRA-1:HOTP-SHA
151             (1|256|512)-
152             (\d+):
153             (C-)?Q
154             (A|N|H)\d+
155             (-PSHA(1|256|512))?
156             (-S(\d+))?
157             (-T(\d+)(S|H|M))?
158             $
159             /x;
160             my $sha = $1;
161             my $digits = $2;
162             my $has_counter = $3;
163             my $question_format = $4;
164             my $has_password = $5;
165             my $password_format = $6;
166             my $has_session = $7;
167             my $session_size = $8;
168             my $has_timestamp = $9;
169             my $period = $10;
170             my $time_unit = $11;
171              
172             #Validate parameters included in the OCRA Suite
173             croak "Must request at least 4 digits" if $digits < 4;
174             croak "Must request at most 10 digits" if $digits > 10;
175              
176             #Validate if additional parameters required
177             #in the provided OCRA Suite are present
178             croak "Parameter \"counter\" is required for the provided ocrasuite"
179             if $has_counter && !defined( $self->{counter} );
180             croak "Parameter \"password\" is required for the provided ocrasuite"
181             if $has_password && !defined( $self->{password} );
182             croak
183             "Parameter \"session_information\" is required for the provided ocrasuite"
184             if $has_session && !defined( $self->{session_information} );
185              
186             #Initiate the data input with the Ocra Suite and the separator byte
187             my $datainput = $self->ocrasuite . "\0";
188              
189             #Concatenate encoded Counter padded with 8 zeros at left
190             $datainput .= _hex_to_bytes( _dec_to_hex( $self->counter ), 8 )
191             if $has_counter;
192              
193             #Encode the Question on the specified format
194             my $question;
195             $question = _str_to_hex( $self->question ) if $question_format eq 'A';
196             $question = _dec_to_hex( $self->question ) if $question_format eq 'N';
197             $question = _check_hex( $self->question ) if $question_format eq 'H';
198              
199             #Concatenate encoded Question padded with 128 zeros at right
200             $datainput .= pack( "H*",
201             _check_hex($question)
202             . "\0" x ( 256 - length( _check_hex($question) ) ) );
203              
204             #Concatenate encoded password and pad with zeros
205             #to the left depending on the specified SHA
206             my %password_size = ( 1 => 20, 256 => 32, 512 => 64 );
207             $datainput
208             .= _hex_to_bytes( $self->password, $password_size{$password_format} )
209             if $has_password;
210              
211             #Concatenate encoded Session Information padded with zeros at left
212             $datainput .= _hex_to_bytes( _str_to_hex( $self->session_information ),
213             $session_size )
214             if $has_session;
215              
216             #Assign timestamp value
217             if ($has_timestamp) {
218             my $timestamp;
219             if ( $self->{timestamp} ) {
220              
221             #use provided timestamp
222             $timestamp = $self->timestamp;
223             }
224             else {
225              
226             #if timestamp is not provided, query the system
227             #time and calculate according to provided parameters
228             my %timestep = ( S => 1, M => 60, H => 3600 );
229             $timestamp = int( time() / ( $period * $timestep{$time_unit} ) );
230             }
231              
232             #Concatenate encoded timestamp padded with 8 zeros at left
233             $datainput .= _hex_to_bytes( _dec_to_hex($timestamp), 8 );
234             }
235              
236             #Encode the Key
237             my $key = pack( 'H*', _check_hex( $self->key ) );
238              
239             #Compute the HMAC
240             my $hash;
241             {
242             no strict 'refs';
243             $hash = &{"hmac_sha$sha"}( $datainput, $key );
244             }
245              
246             #Dynamic Truncation
247             my $offset = hex substr unpack( "H*", $hash ), -1;
248             my $dt = unpack "N" => substr $hash, $offset, 4;
249             $dt &= 0x7fffffff;
250             $dt = Math::BigInt->new($dt);
251             my $modulus = 10**$digits;
252              
253             #Compute the HOTP value
254             return sprintf( "%0${digits}d", $dt->bmod($modulus) );
255              
256             }
257              
258             #Private method, encodes a string in hexadecimal format
259             sub _str_to_hex {
260             my ($str) = @_;
261             return unpack( 'H*', $str );
262              
263             }
264              
265             #Private method, encodes a decimal in hexadecimal format
266             sub _dec_to_hex {
267             my ($dec) = @_;
268             my $big_int = Math::BigInt->new($dec);
269             return _check_hex( $big_int->as_hex() );
270             }
271              
272             #Private method, validates hexadecimal format, removes sign and preceding 0x or x
273             sub _check_hex {
274             my ($num) = @_;
275              
276             if ($num =~ s/
277             ^
278             ( [+-]? )
279             (0?x)?
280             (
281             [0-9a-fA-F]*
282             ( _ [0-9a-fA-F]+ )*
283             )
284             $
285             //x
286             )
287             {
288              
289             return $3;
290             }
291             else { croak "$num: not in hex format"; }
292              
293             }
294              
295             #private method, encodes hexadecimal to binary, pads with zeros at left
296             sub _hex_to_bytes {
297             my ( $hex, $pad ) = @_;
298             $hex = _check_hex($hex);
299             my $length = length($hex);
300              
301             return pack( 'H*', "\0" x ( ( $pad * 2 ) - $length ) . $hex );
302              
303             }
304              
305             =head1 AUTHOR
306              
307             Pascual De Ruvo, C<< <pderuvo at gmail.com> >>
308              
309             =head1 BUGS
310              
311             Please report any bugs or feature requests to C<bug-authen-oath-ocra at rt.cpan.org>, or through
312             the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Authen-OATH-OCRA>. I will be notified, and then you'll
313             automatically be notified of progress on your bug as I make changes.
314              
315              
316             =head1 SUPPORT
317              
318             You can find documentation for this module with the perldoc command.
319              
320             perldoc Authen::OATH::OCRA
321              
322              
323             You can also look for information at:
324              
325             =over 4
326              
327             =item * OATH: Initiative for Open Authentication
328              
329             L<http://www.openauthentication.org>
330              
331             =item * OCRA: OATH Challenge-Response Algorithm RFC
332              
333             L<http://tools.ietf.org/html/rfc6287>
334              
335             =item * RT: CPAN's request tracker
336              
337             L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=Authen-OATH-OCRA>
338              
339             =item * AnnoCPAN: Annotated CPAN documentation
340              
341             L<http://annocpan.org/dist/Authen-OATH-OCRA>
342              
343             =item * CPAN Ratings
344              
345             L<http://cpanratings.perl.org/d/Authen-OATH-OCRA>
346              
347             =item * Search CPAN
348              
349             L<http://search.cpan.org/dist/Authen-OATH-OCRA/>
350              
351             =back
352              
353             =head1 LICENSE AND COPYRIGHT
354              
355             Copyright 2012 Pascual De Ruvo.
356              
357             This program is free software; you can redistribute it and/or modify it
358             under the terms of either: the GNU General Public License as published
359             by the Free Software Foundation; or the Artistic License.
360              
361             See http://dev.perl.org/licenses/ for more information.
362              
363              
364             =cut
365              
366             1; # End of Authen::OATH::OCRA
367              
368             ################################################################################
369             # EOF