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__ |