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