File Coverage

blib/lib/Authen/SCRAM/Role/Common.pm
Criterion Covered Total %
statement 110 119 92.4
branch 11 18 61.1
condition 13 15 86.6
subroutine 37 39 94.8
pod n/a
total 171 191 89.5


line stmt bran cond sub pod time code
1 4     4   38293 use 5.008;
  4         17  
2 4     4   25 use strict;
  4         7  
  4         89  
3 4     4   20 use warnings;
  4         7  
  4         232  
4              
5             package Authen::SCRAM::Role::Common;
6              
7             our $VERSION = '0.009';
8              
9 4     4   24 use Moo::Role 1.001000;
  4         89  
  4         25  
10              
11 4     4   2800 use Authen::SASL::SASLprep 1.100 qw/saslprep/;
  4         187846  
  4         254  
12 4     4   35 use Carp qw/croak/;
  4         10  
  4         177  
13 4     4   1440 use Crypt::URandom qw/urandom/;
  4         9672  
  4         198  
14 4     4   35 use Encode qw/encode_utf8/;
  4         11  
  4         206  
15 4     4   26 use MIME::Base64 qw/encode_base64/;
  4         8  
  4         186  
16 4     4   24 use PBKDF2::Tiny 0.003 qw/digest_fcn hmac/;
  4         86  
  4         173  
17 4     4   26 use Try::Tiny;
  4         6  
  4         203  
18 4     4   28 use Types::Standard qw/Bool Enum Num HashRef CodeRef/;
  4         8  
  4         44  
19              
20 4     4   4288 use namespace::clean;
  4         11  
  4         38  
21              
22             #--------------------------------------------------------------------------#
23             # public attributes
24             #--------------------------------------------------------------------------#
25              
26             has digest => (
27             is => 'ro',
28             isa => Enum [qw/SHA-1 SHA-224 SHA-256 SHA-384 SHA-512/],
29             default => 'SHA-1',
30             );
31              
32             has nonce_size => (
33             is => 'ro',
34             isa => Num,
35             default => 192,
36             );
37              
38             has skip_saslprep => (
39             is => 'ro',
40             isa => Bool,
41             );
42              
43             #--------------------------------------------------------------------------#
44             # private attributes
45             #--------------------------------------------------------------------------#
46              
47             has _const_eq_fcn => (
48             is => 'lazy',
49             isa => CodeRef,
50             );
51              
52             # constant time comparison to avoid timing attacks; uses
53             # String::Compare::ConstantTime if available or a pure-Perl fallback
54             sub _build__const_eq_fcn {
55 7     7   208 my ($self) = @_;
56 7 50       14 if ( eval { require String::Compare::ConstantTime; 1 } ) {
  7         1361  
  7         1286  
57 7         142 return \&String::Compare::ConstantTime::equals;
58             }
59             else {
60             return sub {
61 0     0   0 my ( $dk1, $dk2 ) = @_;
62 0         0 my $dk1_length = length($dk1);
63 0 0       0 return unless $dk1_length == length($dk2);
64 0         0 my $match = 1;
65 0         0 for my $offset ( 0 .. $dk1_length ) {
66 0 0       0 $match &= ( substr( $dk1, $offset, 1 ) eq substr( $dk2, $offset, 1 ) ) ? 1 : 0;
67             }
68 0         0 return $match;
69 0         0 };
70             }
71             }
72              
73             has _digest_fcn => (
74             is => 'lazy',
75             isa => CodeRef,
76             );
77              
78             sub _build__digest_fcn {
79 16     16   191 my ($self) = @_;
80 16         80 my ($fcn) = digest_fcn( $self->digest );
81 16         355 return $fcn;
82             }
83              
84             # _hmac_fcn( $key, $data ) -- this matches RFC 5802 parameter order but
85             # is reversed from Digest::HMAC/PBKDF2::Tiny which uses (data, key)
86             has _hmac_fcn => (
87             is => 'lazy',
88             isa => CodeRef,
89             );
90              
91             sub _build__hmac_fcn {
92 16     16   487 my ($self) = @_;
93 16         118 my ( $fcn, $block_size, $digest_length ) = digest_fcn( $self->digest );
94             return sub {
95 47     47   1262 my ( $key, $data ) = @_;
96 47 50       119 $key = $fcn->($key) if length($key) > $block_size;
97 47         126 return hmac( $data, $key, $fcn, $block_size );
98 16         497 };
99             }
100              
101             # helpful for testing
102             has _nonce_generator => (
103             is => 'lazy',
104             isa => CodeRef,
105             );
106              
107             sub _build__nonce_generator {
108 17     17   150 my ($self) = @_;
109 17     18   264 return sub { return $self->_base64( urandom( $self->nonce_size / 8 ) ) };
  18         450  
110             }
111              
112             # _session builds up parameters used during a SCRAM session. Keys
113             # starting with "_" are private state not used for exchange. Single
114             # letter keys are defined as per RFC5802
115             #
116             # _nonce private nonce part
117             # _c1b client-first-message-bare
118             # _s1 server-first-message
119             # _c2wop client-final-message-without-proof
120             # _stored_key H(ClientKey)
121             # _server_key HMAC(SaltedPassword, "Server Key")
122             # _auth AuthMessage
123              
124             has _session => (
125             is => 'lazy',
126             isa => HashRef,
127             clearer => 1,
128             );
129              
130             sub _build__session {
131 37     37   318 my ($self) = @_;
132 37         523 return { _nonce => $self->_nonce_generator->() };
133             }
134              
135             #--------------------------------------------------------------------------#
136             # methods
137             #--------------------------------------------------------------------------#
138              
139             sub _auth_msg {
140 29     29   271 my ($self) = @_;
141             return $self->_session->{_auth} ||=
142 29   66     489 encode_utf8( join( ",", map { $self->_session->{$_} } qw/_c1b _s1 _c2wop/ ) );
  51         1097  
143             }
144              
145             sub _base64 {
146 66     66   33523 my ( $self, $data ) = @_;
147 66         659 return encode_base64( $data, "" );
148             }
149              
150             sub _client_sig {
151 17     17   48 my ($self) = @_;
152 17         321 return $self->_hmac_fcn->( $self->_session->{_stored_key}, $self->_auth_msg );
153             }
154              
155             sub _construct_gs2 {
156 24     24   518 my ( $self, $authz ) = @_;
157 24 100 100     129 my $maybe =
158             ( defined($authz) && length($authz) )
159             ? ( "a=" . $self->_encode_name($authz) )
160             : "";
161 24         304 return "n,$maybe,";
162             }
163              
164             sub _decode_name {
165 11     11   30 my ( $self, $name ) = @_;
166 11         27 $name =~ s/=2c/,/g;
167 11         26 $name =~ s/=3d/=/g;
168 11         34 return $name;
169             }
170              
171             sub _encode_name {
172 36     36   71 my ( $self, $name ) = @_;
173 36         80 $name =~ s/=/=3d/g;
174 36         63 $name =~ s/,/=2c/g;
175 36         72 return $name;
176             }
177              
178             sub _extend_nonce {
179 8     8   21 my ($self) = @_;
180 8         145 $self->_session->{r} .= $self->_session->{_nonce};
181             }
182              
183             sub _get_session {
184 189     189   861 my ( $self, $key ) = @_;
185 189         2968 return $self->_session->{$key};
186             }
187              
188             sub _join_reply {
189 63     63   149 my ( $self, @fields ) = @_;
190 63         94 my @reply;
191 63         163 for my $k (@fields) {
192 138         1972 my $v = $self->_session->{$k};
193 138 100 66     1170 if ( $k eq 'a' || $k eq 'n' ) {
194 29         72 $v = $self->_encode_name($v);
195             }
196 138         403 push @reply, "$k=$v";
197             }
198 63         241 my $msg = '' . join( ",", @reply );
199 63         179 utf8::upgrade($msg);
200 63         246 return $msg;
201             }
202              
203             sub _parse_to_session {
204 39     39   102 my ( $self, @params ) = @_;
205 39         103 for my $part (@params) {
206 98         847 my ( $k, $v ) = split /=/, $part, 2;
207 98 100 100     582 if ( $k eq 'a' || $k eq 'n' ) {
    100 100        
208 11         40 $v = $self->_saslprep( $self->_decode_name($v) );
209             }
210             elsif ( $k eq 'i' && $v !~ /^[0-9]+$/ ) {
211 3         220 croak "SCRAM iteration parameter '$part' invalid";
212             }
213 95         1574 $self->_session->{$k} = $v;
214             }
215 36         314 return;
216             }
217              
218             sub _saslprep {
219 54     54   136 my ( $self, $name ) = @_;
220              
221 54 50       167 return $name if $self->skip_saslprep;
222              
223             my $prepped = try {
224 54     54   2258 saslprep( $name, 1 ); # '1' makes it use stored mode
225             }
226             catch {
227 0     0   0 croak "SCRAM username '$name' invalid: $_";
228 54         370 };
229 54         12229 return $prepped;
230             }
231              
232             sub _set_session {
233 133     133   1275 my ( $self, %args ) = @_;
234 133         452 while ( my ( $k, $v ) = each %args ) {
235 212         3621 $self->_session->{$k} = $v;
236             }
237 133         1226 return;
238             }
239              
240             #--------------------------------------------------------------------------#
241             # regular expressions for parsing
242             #--------------------------------------------------------------------------#
243              
244             # tokens
245             my $VALUE = qr/[^,]+/;
246             my $CBNAME = qr/[a-zA-Z0-9.-]+/;
247             my $ATTR_VAL = qr/[a-zA-Z]=$VALUE/;
248              
249             # atoms
250             my $GS2_CBIND_FLAG = qr/(?:n|y|p=$VALUE)/;
251             my $AUTHZID = qr/a=$VALUE/;
252             my $CHN_BIND = qr/c=$VALUE/;
253             my $S_ERROR = qr/e=$VALUE/;
254             my $ITER_CNT = qr/i=$VALUE/;
255             my $MEXT = qr/m=$VALUE/;
256             my $USERNAME = qr/n=$VALUE/;
257             my $PROOF = qr/p=$VALUE/;
258             my $NONCE = qr/r=$VALUE/;
259             my $SALT = qr/s=$VALUE/;
260             my $VERIFIER = qr/v=$VALUE/;
261             my $EXT = qr/$ATTR_VAL (?: , $ATTR_VAL)*/;
262              
263             # constructions
264             my $C_FRST_BARE = qr/(?:($MEXT),)? ($USERNAME) , ($NONCE) (?:,$EXT)?/x;
265             my $GS2_HEADER = qr/($GS2_CBIND_FLAG) , ($AUTHZID)? , /x;
266             my $C_FINL_WO_PRF = qr/($CHN_BIND) , ($NONCE) (?:,$EXT)?/x;
267              
268             # messages
269             my $C_FRST_MSG = qr/$GS2_HEADER ($C_FRST_BARE)/x;
270             my $S_FRST_MSG = qr/(?:($MEXT),)? ($NONCE) , ($SALT) , ($ITER_CNT) (?:,$EXT)?/x;
271             my $C_FINL_MSG = qr/($C_FINL_WO_PRF) , ($PROOF)/x;
272             my $S_FINL_MSG = qr/($S_ERROR | $VERIFIER)/x;
273              
274 17     17   167 sub _client_first_re { $C_FRST_MSG } # ($cbind, $authz?, $c_1_bare, $mext?, @params)
275 23     23   224 sub _server_first_re { $S_FRST_MSG } # ($mext?, @params)
276 7     7   143 sub _client_final_re { $C_FINL_MSG } # ($c_2_wo_proof, @params)
277 7     7   98 sub _server_final_re { $S_FINL_MSG } # ($error_or_verification)
278              
279             1;
280              
281             =pod
282              
283             =for Pod::Coverage digest nonce_size skip_saslprep
284              
285             =cut
286              
287             # vim: ts=4 sts=4 sw=4 et: