File Coverage

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


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