File Coverage

blib/lib/Authen/SCRAM/Role/Common.pm
Criterion Covered Total %
statement 116 117 99.1
branch 13 16 81.2
condition 13 15 86.6
subroutine 38 39 97.4
pod n/a
total 180 187 96.2


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