File Coverage

blib/lib/Authen/SCRAM/Client.pm
Criterion Covered Total %
statement 88 90 97.7
branch 12 14 85.7
condition 2 3 66.6
subroutine 19 19 100.0
pod 4 4 100.0
total 125 130 96.1


line stmt bran cond sub pod time code
1 3     3   4258 use 5.008;
  3         8  
2 3     3   10 use strict;
  3         5  
  3         55  
3 3     3   12 use warnings;
  3         4  
  3         139  
4              
5             package Authen::SCRAM::Client;
6             # ABSTRACT: RFC 5802 SCRAM client
7              
8             our $VERSION = '0.011';
9              
10 3     3   1211 use Moo 1.001000;
  3         22969  
  3         15  
11              
12 3     3   3261 use Carp qw/croak/;
  3         6  
  3         108  
13 3     3   15 use Encode qw/encode_utf8/;
  3         4  
  3         101  
14 3     3   13 use MIME::Base64 qw/decode_base64/;
  3         5  
  3         136  
15 3     3   16 use PBKDF2::Tiny 0.003 qw/derive/;
  3         60  
  3         110  
16 3     3   15 use Try::Tiny;
  3         6  
  3         171  
17 3     3   1287 use Types::Standard qw/Str Num/;
  3         206064  
  3         32  
18              
19 3     3   3685 use namespace::clean;
  3         25369  
  3         16  
20              
21             #--------------------------------------------------------------------------#
22             # public attributes
23             #--------------------------------------------------------------------------#
24              
25             #pod =attr username (required)
26             #pod
27             #pod Authentication identity. This will be normalized with the SASLprep algorithm
28             #pod before being transmitted to the server.
29             #pod
30             #pod =cut
31              
32             has username => (
33             is => 'ro',
34             isa => Str,
35             required => 1,
36             );
37              
38             #pod =attr password (required)
39             #pod
40             #pod Authentication password. This will be normalized with the SASLprep algorithm
41             #pod before being transmitted to the server.
42             #pod
43             #pod =cut
44              
45             has password => (
46             is => 'ro',
47             isa => Str,
48             required => 1,
49             );
50              
51             #pod =attr authorization_id
52             #pod
53             #pod If the authentication identity (C) will act as a different,
54             #pod authorization identity, this attribute provides the authorization identity. It
55             #pod is optional. If not provided, the authentication identity is considered by the
56             #pod server to be the same as the authorization identity.
57             #pod
58             #pod =cut
59              
60             has authorization_id => (
61             is => 'ro',
62             isa => Str,
63             default => '',
64             );
65              
66             #pod =attr minimum_iteration_count
67             #pod
68             #pod If the server requests an iteration count less than this value, the client
69             #pod throws an error. This protects against downgrade attacks. The default is
70             #pod 4096, consistent with recommendations in the RFC.
71             #pod
72             #pod =cut
73              
74             has minimum_iteration_count => (
75             is => 'ro',
76             isa => Num,
77             default => 4096,
78             );
79              
80             # The derived PBKDF2 password can be reused if the salt and iteration count
81             # is the same as a previous authentication conversation.
82             has _cached_credentials => (
83             is => 'rw',
84             default => sub { [ "", 0, "" ] }, # salt, iterations, derived password
85             );
86              
87             #--------------------------------------------------------------------------#
88             # provided by Authen::SCRAM::Role::Common
89             #--------------------------------------------------------------------------#
90              
91             with 'Authen::SCRAM::Role::Common';
92              
93             #pod =attr digest
94             #pod
95             #pod Name of a digest function available via L. Valid values are
96             #pod SHA-1, SHA-224, SHA-256, SHA-384, or SHA-512. Defaults to SHA-1.
97             #pod
98             #pod =attr nonce_size
99             #pod
100             #pod Size of the client-generated nonce, in bits. Defaults to 192.
101             #pod The server-nonce will be appended, so the final nonce size will
102             #pod be substantially larger.
103             #pod
104             #pod =attr skip_saslprep
105             #pod
106             #pod A boolean that defaults to false. If set to true, usernames and passwords will
107             #pod not be normalized through SASLprep. This is a deviation from the RFC5802 spec
108             #pod and is not recommended.
109             #pod
110             #pod =cut
111              
112             #--------------------------------------------------------------------------#
113             # private attributes
114             #--------------------------------------------------------------------------#
115              
116             has _prepped_user => (
117             is => 'lazy',
118             isa => Str,
119             );
120              
121             sub _build__prepped_user {
122 17     17   153 my ($self) = @_;
123 17         87 return $self->_saslprep( $self->username );
124             }
125              
126             has _prepped_pass => (
127             is => 'lazy',
128             isa => Str,
129             );
130              
131             sub _build__prepped_pass {
132 9     9   87 my ($self) = @_;
133 9         30 return $self->_saslprep( $self->password );
134             }
135              
136             has _prepped_authz => (
137             is => 'lazy',
138             isa => Str,
139             );
140              
141             sub _build__prepped_authz {
142 17     17   147 my ($self) = @_;
143 17         78 return $self->_saslprep( $self->authorization_id );
144             }
145              
146             has _gs2_header => (
147             is => 'lazy',
148             isa => Str,
149             );
150              
151             sub _build__gs2_header {
152 17     17   137 my ($self) = @_;
153 17         217 return $self->_construct_gs2( $self->_prepped_authz );
154             }
155              
156             #--------------------------------------------------------------------------#
157             # public methods
158             #--------------------------------------------------------------------------#
159              
160             #pod =method first_msg
161             #pod
162             #pod $client_first_msg = $client->first_msg();
163             #pod
164             #pod This takes no arguments and returns the C character
165             #pod string to be sent to the server to initiate a SCRAM session. Calling this
166             #pod again will reset the internal state and initiate a new session. This will
167             #pod throw an exception should an error occur.
168             #pod
169             #pod =cut
170              
171             sub first_msg {
172 29     29 1 12582 my ($self) = @_;
173              
174 29         537 $self->_clear_session;
175 29         473 $self->_set_session(
176             n => $self->_prepped_user,
177             r => $self->_get_session('_nonce'),
178             );
179 29         74 my $c_1_bare = $self->_join_reply(qw/n r/);
180 29         72 $self->_set_session( _c1b => $c_1_bare );
181 29         372 my $msg = $self->_gs2_header . $c_1_bare;
182 29         483 utf8::upgrade($msg); # ensure UTF-8 encoding internally
183 29         75 return $msg;
184             }
185              
186             #pod =method final_msg
187             #pod
188             #pod $client_final_msg = $client->final_msg( $server_first_msg );
189             #pod
190             #pod This takes the C character string received from the
191             #pod server and returns the C character string containing the
192             #pod authentication proof to be sent to the server. This will throw an exception
193             #pod should an error occur.
194             #pod
195             #pod =cut
196              
197             sub final_msg {
198 23     23 1 4935 my ( $self, $s_first_msg ) = @_;
199              
200 23         62 my ( $mext, @params ) = $s_first_msg =~ $self->_server_first_re;
201              
202 23 100       66 if ( defined $mext ) {
203 1         101 croak
204             "SCRAM server-first-message required mandatory extension '$mext', but we do not support it";
205             }
206 22 100       51 if ( !@params ) {
207 5         334 croak "SCRAM server-first-message could not be parsed";
208             }
209              
210 17         40 my $original_nonce = $self->_get_session("r");
211 17         141 $self->_parse_to_session(@params);
212              
213 14         29 my $joint_nonce = $self->_get_session("r");
214 14 100       286 unless ( $joint_nonce =~ m{^\Q$original_nonce\E.} ) {
215 2         136 croak "SCRAM server-first-message nonce invalid";
216             }
217              
218             # assemble client-final-wo-proof
219             $self->_set_session(
220 12         175 _s1 => $s_first_msg,
221             c => $self->_base64( encode_utf8( $self->_gs2_header ) ),
222             );
223 12         35 $self->_set_session( '_c2wop' => $self->_join_reply(qw/c r/) );
224              
225             # assemble proof
226 12         29 my $salt = decode_base64( $self->_get_session("s") );
227 12         97 my $iters = $self->_get_session("i");
228 12 100       114 if ( $iters < $self->minimum_iteration_count ) {
229 2         238 croak sprintf( "SCRAM server requested %d iterations, less than the minimum of %d",
230             $iters, $self->minimum_iteration_count );
231             }
232              
233 10         28 my ( $stored_key, $client_key, $server_key ) = $self->computed_keys( $salt, $iters );
234              
235 10         61 $self->_set_session(
236             _stored_key => $stored_key,
237             _server_key => $server_key,
238             );
239              
240 10         69 my $client_sig = $self->_client_sig;
241              
242 10         161 $self->_set_session( p => $self->_base64( $client_key ^ $client_sig ) );
243              
244 10         43 return $self->_join_reply(qw/c r p/);
245             }
246              
247             #pod =method validate
248             #pod
249             #pod $client->validate( $server_final_msg );
250             #pod
251             #pod This takes the C character string received from the
252             #pod server and verifies that the server actually has a copy of the client
253             #pod credentials. It will return true if valid and throw an exception, otherwise.
254             #pod
255             #pod =cut
256              
257             sub validate {
258 7     7 1 3765 my ( $self, $s_final_msg ) = @_;
259              
260 7         28 my (@params) = $s_final_msg =~ $self->_server_final_re;
261 7         23 $self->_parse_to_session(@params);
262              
263 7 50       22 if ( my $err = $self->_get_session("e") ) {
264 0         0 croak "SCRAM server-final-message was error '$err'";
265             }
266              
267 7         60 my $server_sig =
268             $self->_hmac_fcn->( $self->_get_session("_server_key"), $self->_auth_msg );
269              
270 7 50       107 if ( $self->_base64($server_sig) ne $self->_get_session("v") ) {
271 0         0 croak "SCRAM server-final-message failed validation";
272             }
273              
274 7         68 return 1;
275             }
276              
277             #pod =method computed_keys
278             #pod
279             #pod This method returns the opaque keys used in the SCRAM protocol. It returns
280             #pod the 'stored key', the 'client key' and the 'server key'. The server must
281             #pod have a copy of the stored key and server key for a given user in order to
282             #pod authenticate.
283             #pod
284             #pod This method caches the computed values -- it generates them fresh only if
285             #pod the supplied salt and iteration count don't match the cached salt and
286             #pod iteration count.
287             #pod
288             #pod =cut
289              
290             sub computed_keys {
291 10     10 1 25 my ( $self, $salt, $iters ) = @_;
292 10         25 my $cache = $self->_cached_credentials;
293              
294 10 100 66     36 if ( $cache->[0] eq $salt && $cache->[1] == $iters ) {
295             # return stored key, client key, server key
296 1         3 return @{$cache}[ 2 .. 4 ];
  1         4  
297             }
298              
299 9         160 my $salted_pw =
300             derive( $self->digest, encode_utf8( $self->_prepped_pass ), $salt, $iters );
301 9         331702 my $client_key = $self->_hmac_fcn->( $salted_pw, "Client Key" );
302 9         244 my $server_key = $self->_hmac_fcn->( $salted_pw, "Server Key" );
303 9         274 my $stored_key = $self->_digest_fcn->($client_key);
304              
305 9         283 $self->_cached_credentials(
306             [ $salt, $iters, $stored_key, $client_key, $server_key ] );
307              
308 9         56 return ( $stored_key, $client_key, $server_key );
309             }
310              
311             1;
312              
313              
314             # vim: ts=4 sts=4 sw=4 et:
315              
316             __END__