File Coverage

blib/lib/Connector/Builtin/Authentication/PasswordScheme.pm
Criterion Covered Total %
statement 92 108 85.1
branch 25 34 73.5
condition 2 6 33.3
subroutine 12 13 92.3
pod 3 3 100.0
total 134 164 81.7


line stmt bran cond sub pod time code
1             # Connector::Builtin::Authentication::PasswordScheme
2             #
3             # Check passwords against a file with salted hashes and scheme prefix
4             #
5              
6             use strict;
7 1     1   130240 use warnings;
  1         10  
  1         24  
8 1     1   4 use English;
  1         6  
  1         24  
9 1     1   4 use Data::Dumper;
  1         1  
  1         6  
10 1     1   886  
  1         5818  
  1         50  
11             use MIME::Base64;
12 1     1   429 use Digest::SHA;
  1         533  
  1         48  
13 1     1   469 use Digest::MD5;
  1         2662  
  1         39  
14 1     1   7  
  1         3  
  1         38  
15             use Moose;
16 1     1   502 extends 'Connector::Builtin';
  1         398609  
  1         7  
17              
18             my $self = shift;
19              
20 0     0   0 if (! -r $self->{LOCATION}) {
21             confess("Cannot open input file " . $self->{LOCATION} . " for reading.");
22 0 0       0 }
23 0         0  
24             return 1;
25             }
26 0         0  
27             my $self = shift;
28             my $arg = shift;
29             my $params = shift;
30 10     10 1 29  
31 10         18 my @path = $self->_build_path( $arg );
32 10         13 my $user = shift @path;
33              
34 10         49 my $password = $params->{password};
35 10         20  
36              
37 10         20 if (!$user) {
38             $self->log()->error('No username');
39             die "no username given";
40 10 50       19 }
41 0         0  
42 0         0 if (!$password) {
43             $self->log()->error('No password');
44             die "no password given";
45 10 100       18 }
46 1         21  
47 1         20  
48             $self->log()->debug('verify password for ' . $user );
49              
50             if ($user =~ /[^a-zA-Z0-9_\-\.\@]/) {
51 9         184 $self->log()->error('Invalid chars in username ('.$user.')');
52             return $self->_node_not_exists( $user );
53 9 100       68 }
54 1         19  
55 1         9 my $filename = $self->{LOCATION};
56              
57             if (! -r $filename || ! open FILE, "$filename") {
58 8         16 $self->log()->error('Can\'t open/read from file ' . $filename);
59             die 'Can\'t open/read from file ' . $filename;
60 8 50 33     463 }
61 0         0  
62 0         0 while (<FILE>) {
63             if (/^$user:/) {
64             chomp;
65 8         150 my @t = split(/:/, $_, 3);
66 23 100       177 $self->log()->trace('found line ' . Dumper @t);
67 6         16  
68 6         16 # This code is mainly a copy of OpenXPKI::Server::Authentication::Password
69 6         144 # but we do not support unsalted passwords
70             # digest specified in RFC 2307 userPassword notation?
71             my $encrypted;
72             my $scheme;
73             if ($t[1] =~ m{ \{ (\w+) \} (.+) }xms) {
74 6         289 $scheme = lc($1);
75             $encrypted = $2;
76 6 50       33 } else {
77 6         17 $self->log()->error('unparsable entry ' . $t[1]);
78 6         13 return 0;
79             }
80 0         0  
81 0         0 my ($computed_secret, $salt);
82             eval {
83             if ($scheme eq 'ssha') {
84 6         10 $salt = substr(decode_base64($encrypted), 20);
85 6         10 my $ctx = Digest::SHA->new();
86 6 100       23 $ctx->add($password);
    100          
    50          
87 2         16 $ctx->add($salt);
88 2         15 $computed_secret = encode_base64($ctx->digest() . $salt, '');
89 2         56 } elsif ($scheme eq 'smd5') {
90 2         7 $salt = substr(decode_base64($encrypted), 16);
91 2         28 my $ctx = Digest::MD5->new();
92             $ctx->add($password);
93 1         4 $ctx->add($salt);
94 1         8 $computed_secret = encode_base64($ctx->digest() . $salt, '');
95 1         6 } elsif ($scheme eq 'crypt') {
96 1         6 $computed_secret = crypt($password, $encrypted);
97 1         19 } else {
98             $self->log()->error('unsupported scheme' . $scheme);
99 3         483 return 0;
100             }
101 0         0 };
102 0         0  
103             $self->log()->debug('eval failed ' . $EVAL_ERROR->message()) if ($EVAL_ERROR);
104              
105             if (! defined $computed_secret) {
106 6 50       18 $self->log()->error('unable to compute secret using scheme ' . $scheme);
107             return 0;
108 6 50       14 }
109 0         0  
110 0         0 ##! 2: "ident user ::= $account and digest ::= $computed_secret"
111             $computed_secret =~ s{ =+ \z }{}xms;
112             $encrypted =~ s{ =+ \z }{}xms;
113              
114 6         17 ## compare passphrases
115 6         11 if ($computed_secret eq $encrypted) {
116             $self->log()->info('Password accepted for ' . $user);
117             return 1;
118 6 100       14 } else {
119 4         95 $self->log()->info('Password mismatch for ' . $user);
120 4         45 return 0;
121             }
122 2         43 }
123 2         22 }
124             return $self->_node_not_exists( $user );
125             }
126              
127 2         17 my $self = shift;
128              
129             # If we have no path, we tell the caller that we are a connector
130             my @path = $self->_build_path( shift );
131 2     2 1 278 if (scalar @path == 0) {
132             return { TYPE => "connector" };
133             }
134 2         7  
135 2 100       9 return {TYPE => "scalar" };
136 1         8 }
137              
138              
139 1         6 my $self = shift;
140              
141             # No path = connector root which always exists
142             my @path = $self->_build_path( shift );
143             if (scalar @path == 0) {
144 4     4 1 9 return 1;
145             }
146              
147 4         9 my $user = shift @path;
148 4 100       11  
149 1         5 my $filename = $self->{LOCATION};
150             if (! -r $filename || ! open FILE, "$filename") {
151             $self->log()->error('Can\'t open/read from file ' . $filename);
152 3         6 return 0;
153             }
154 3         7  
155 3 50 33     182 while (<FILE>) {
156 0         0 if (/^$user:/) {
157 0         0 return 1;
158             }
159             }
160 3         48 return 0;
161 7 100       64 }
162 2         12  
163             no Moose;
164             __PACKAGE__->meta->make_immutable;
165 1         7  
166             1;
167              
168 1     1   7574 =head1 Name
  1         2  
  1         5  
169              
170             Connector::Builtin::Authentication::PasswordScheme
171              
172             =head1 Description
173              
174             Lightweight connector to check passwords against a password file holding
175             username/password pairs where the password is encrypted using a salted hash.
176             Password notation follows RFC2307 ({scheme}saltedpassword) but we support
177             only salted schemes: smd5, ssha and crypt.
178              
179             =head2 Usage
180              
181             The username is the first component of the path, the password needs to be
182             passed in the extended parameters using the key password.
183              
184             Example:
185              
186             $connector->get('username', { password => 'mySecret' } );
187              
188             =head2 Return values
189              
190             1 if the password matches, 0 if the user is found but the password does not
191             match and undef if the user is not found.
192              
193             The connector will die if the password file is not readable or if one of
194             the parameters is missing.
195              
196             =head2 Limitations
197              
198             Usernames are limited to [a-zA-Z0-9_\-\.], invalid names are treated as not
199             found.
200