File Coverage

blib/lib/Authen/Passphrase/VMSPurdy.pm
Criterion Covered Total %
statement 82 87 94.2
branch 37 60 61.6
condition 6 18 33.3
subroutine 20 20 100.0
pod 10 10 100.0
total 155 195 79.4


line stmt bran cond sub pod time code
1             =head1 NAME
2              
3             Authen::Passphrase::VMSPurdy - passphrases with the VMS Purdy polynomial
4             system
5              
6             =head1 SYNOPSIS
7              
8             use Authen::Passphrase::VMSPurdy;
9              
10             $ppr = Authen::Passphrase::VMSPurdy->new(
11             username => "jrandom", salt => 25362,
12             hash_hex => "832a0c270179584a");
13              
14             $ppr = Authen::Passphrase::VMSPurdy->new(
15             username => "jrandom", salt_random => 1,
16             passphrase => "passphrase");
17              
18             $ppr = Authen::Passphrase::VMSPurdy->from_crypt(
19             '$VMS3$1263832A0C270179584AJRANDOM');
20              
21             $ppr = Authen::Passphrase::VMSPurdy->from_rfc2307(
22             '{CRYPT}$VMS3$1263832A0C270179584AJRANDOM');
23              
24             $algorithm = $ppr->algorithm;
25             $username = $ppr->username;
26             $salt = $ppr->salt;
27             $hash = $ppr->hash;
28             $hash_hex = $ppr->hash_hex;
29              
30             if($ppr->match($passphrase)) { ...
31              
32             $passwd = $ppr->as_crypt;
33             $userPassword = $ppr->as_rfc2307;
34              
35             =head1 DESCRIPTION
36              
37             An object of this class encapsulates a passphrase hashed using one of
38             the Purdy polynomial hash functions used in VMS. This is a subclass
39             of L, and this document assumes that the reader is
40             familiar with the documentation for that class.
41              
42             The core of the Purdy polynomial hashing algorithm transforms
43             one 64-bit number into another 64-bit number. It was
44             developed by George B. Purdy, and described in the paper
45             "A High Security Log-in Procedure" which can be found at
46             L.
47              
48             For practical use in passphrase hashing, the Purdy polynomial must
49             be augmented by a procedure to turn a variable-length passphrase
50             into the initial 64-bit number to be hashed. In VMS this pre-hashing
51             phase also incorporates the username of the account to which access is
52             being controlled, in order to prevent identical passphrases yielding
53             identical hashes. This is a form of salting. Another salt parameter,
54             a 16-bit integer, is also included, this one going under the name "salt".
55              
56             There are three variants of the pre-hashing algorithm. The original
57             version, known as "B" and used during field testing of VMS 2.0,
58             truncates or space-pads the username to a fixed length. The second
59             version, known as "B" and used from VMS 2.0 up to (but not
60             including) VMS 5.4, properly handles the variable-length nature of
61             the username. The third version, known as "B" and used from
62             VMS 5.4 onwards, performs some extra bit rotations to avoid aliasing
63             problems when pre-hashing long strings. All three versions are supported
64             by this module.
65              
66             VMS heavily restricts the composition of both usernames and passphrases.
67             They may only contain alphanumerics, "B<$>", and "B<_>". Case is
68             insignificant. Usernames must be between 1 and 31 characters long,
69             and passphrases must be between 1 and 32 characters long. This module
70             enforces these rules. An invalid passphrase is never accepted as
71             matching.
72              
73             =cut
74              
75             package Authen::Passphrase::VMSPurdy;
76              
77 2     2   102163 { use 5.006; }
  2         10  
78 2     2   15 use warnings;
  2         3  
  2         127  
79 2     2   11 use strict;
  2         5  
  2         79  
80              
81 2     2   1096 use Authen::DecHpwd 2.003 qw(lgi_hpwd UAI_C_PURDY UAI_C_PURDY_V UAI_C_PURDY_S);
  2         13176  
  2         223  
82 2     2   693 use Authen::Passphrase 0.003;
  2         43  
  2         83  
83 2     2   15 use Carp qw(croak);
  2         5  
  2         141  
84 2     2   553 use Crypt::SysRandom 'random_bytes';
  2         3538  
  2         135  
85              
86             our $VERSION = "0.009";
87              
88 2     2   12 use parent "Authen::Passphrase";
  2         3  
  2         13  
89              
90             =head1 CONSTRUCTORS
91              
92             =over
93              
94             =item Authen::Passphrase::VMSPurdy->new(ATTR => VALUE, ...)
95              
96             Generates a new passphrase recogniser object using the VMS Purdy
97             polynomial algorithm family. The following attributes may be given:
98              
99             =over
100              
101             =item B
102              
103             A string indicating which variant of the algorithm is to be used.
104             Valid values are "B" (the original), "B" (modified to
105             use full length of the username), and "B" (extra rotations to
106             avoid aliasing when processing long strings). Default "B".
107              
108             =item B
109              
110             A string to be used as the `username' salt parameter. It is limited to
111             VMS username syntax.
112              
113             =item B
114              
115             The salt, as an integer in the range [0, 65536).
116              
117             =item B
118              
119             The salt, as a string of four hexadecimal digits. The first two
120             digits must give the least-significant byte and the last two give
121             the most-significant byte, with most-significant nybble first within
122             each byte.
123              
124             =item B
125              
126             Causes salt to be generated randomly. The value given for this attribute
127             is ignored.
128              
129             =item B
130              
131             The hash, as a string of eight bytes.
132              
133             =item B
134              
135             The hash, as a string of 16 hexadecimal digits.
136              
137             =item B
138              
139             A passphrase that will be accepted. It is limited to VMS passphrase
140             syntax.
141              
142             =back
143              
144             The username and salt must be given, and either the hash or the
145             passphrase.
146              
147             =cut
148              
149             sub new {
150 15     15 1 197754 my $class = shift;
151 15         45 my $self = bless({}, $class);
152 15         33 my $passphrase;
153 15         64 while(@_) {
154 54         110 my $attr = shift;
155 54         103 my $value = shift;
156 54 100       247 if($attr eq "algorithm") {
    100          
    100          
    100          
    100          
    50          
    100          
    50          
157             croak "algorithm specified redundantly"
158 9 50       37 if exists $self->{algorithm};
159 9 50       67 $value =~ m#\APURDY(?:|_V|_S)\z#
160             or croak "not a valid algorithm";
161 9         88 $self->{algorithm} = "$value";
162             } elsif($attr eq "username") {
163             croak "username specified redundantly"
164 15 50       59 if exists $self->{username};
165 15 50       93 $value =~ m#\A[_\$0-9A-Za-z]{1,31}\z#
166             or croak "not a valid VMS username";
167 15         73 $self->{username} = uc("$value");
168             } elsif($attr eq "salt") {
169             croak "salt specified redundantly"
170 12 50       43 if exists $self->{salt};
171 12 50 33     115 $value == int($value) && $value >= 0 && $value < 65536
      33        
172             or croak "not a valid salt";
173 12         45 $self->{salt} = 0+$value;
174             } elsif($attr eq "salt_hex") {
175             croak "salt specified redundantly"
176 2 50       10 if exists $self->{salt};
177 2 50       11 $value =~ /\A([0-9a-fA-F]{2})([0-9a-fA-F]{2})\z/
178             or croak "not a valid salt";
179 2         13 $self->{salt} = hex($2.$1);
180             } elsif($attr eq "salt_random") {
181             croak "salt specified redundantly"
182 1 50       5 if exists $self->{salt};
183 1         22 $self->{salt} = unpack "S", random_bytes(2);
184             } elsif($attr eq "hash") {
185             croak "hash specified redundantly"
186 0 0 0     0 if exists($self->{hash}) ||
187             defined($passphrase);
188 0 0       0 $value =~ m#\A[\x00-\xff]{8}\z#
189             or croak "not a valid raw hash";
190 0         0 $self->{hash} = "$value";
191             } elsif($attr eq "hash_hex") {
192             croak "hash specified redundantly"
193 6 50 33     38 if exists($self->{hash}) ||
194             defined($passphrase);
195 6 50       75 $value =~ m#\A[0-9A-Fa-f]{16}\z#
196             or croak "not a valid hexadecimal hash";
197 6         46 $self->{hash} = pack("H*", $value);
198             } elsif($attr eq "passphrase") {
199             croak "passphrase specified redundantly"
200 9 50 33     48 if exists($self->{hash}) ||
201             defined($passphrase);
202 9 100       29 $self->_passphrase_acceptable($value)
203             or croak "can't accept that passphrase";
204 5         18 $passphrase = $value;
205             } else {
206 0         0 croak "unrecognised attribute `$attr'";
207             }
208             }
209 11 100       42 $self->{algorithm} = "PURDY_S" unless exists $self->{algorithm};
210 11 50       38 croak "username not specified" unless exists $self->{username};
211 11 50       36 croak "salt not specified" unless exists $self->{salt};
212 11 100       39 $self->{hash} = $self->_hash_of($passphrase)
213             if defined $passphrase;
214 11 50       33 croak "hash not specified" unless exists $self->{hash};
215 11         59 return $self;
216             }
217              
218             =item Authen::Passphrase::VMSPurdy->from_crypt(PASSWD)
219              
220             Generates a new passphrase recogniser object using the VMS Purdy
221             polynomial algorithm family, from a crypt string. The string must
222             consist of an algorithm identifier, the salt in hexadecimal, the hash
223             in hexadecimal, then the username. The salt must be given as four
224             hexadecimal digits, the first two giving the least-significant byte and
225             the last two giving the most-significant byte, with most-significant
226             nybble first within each byte. The algorithm identifier must be
227             "B<$VMS1$>" for "B", "B<$VMS2$>" for "B", or "B<$VMS3$>"
228             for "B". The whole crypt string must be uppercase.
229              
230             =cut
231              
232             my %decode_crypt_alg_num = (
233             "1" => "PURDY",
234             "2" => "PURDY_V",
235             "3" => "PURDY_S",
236             );
237              
238             sub from_crypt {
239 2     2 1 8 my($class, $passwd) = @_;
240 2 50       16 if($passwd =~ /\A\$VMS([123])\$/) {
241 2         6 my $alg = $1;
242 2 50       14 $passwd =~ /\A\$VMS[123]\$([0-9A-F]{4})
243             ([0-9A-F]{16})([_\$0-9A-Z]{1,31})\z/x
244             or croak "malformed \$VMS${alg}\$ data";
245 2         12 my($salt, $hash, $un) = ($1, $2, $3);
246 2         11 return $class->new(algorithm => $decode_crypt_alg_num{$alg},
247             username => $un, salt_hex => $salt, hash_hex => $hash);
248             }
249 0         0 return $class->SUPER::from_crypt($passwd);
250             }
251              
252             =item Authen::Passphrase::VMSPurdy->from_rfc2307(USERPASSWORD)
253              
254             Generates a new passphrase recogniser object using the VMS Purdy
255             polynomial algorithm family, from an RFC 2307 string. The string must
256             consist of "B<{CRYPT}>" (case insensitive) followed by an acceptable
257             crypt string.
258              
259             =back
260              
261             =head1 METHODS
262              
263             =over
264              
265             =item $ppr->algorithm
266              
267             Returns the algorithm variant identifier string. It may be "B"
268             (the original), "B" (modified to use full length of the
269             username), and "B" (extra rotations to avoid aliasing when
270             processing long strings).
271              
272             =cut
273              
274             sub algorithm {
275 11     11 1 4480 my($self) = @_;
276 11         66 return $self->{algorithm};
277             }
278              
279             =item $ppr->username
280              
281             Returns the username string. All alphabetic characters in it are
282             uppercase, which is the canonical form.
283              
284             =cut
285              
286             sub username {
287 11     11 1 40 my($self) = @_;
288 11         69 return $self->{username};
289             }
290              
291             =item $ppr->salt
292              
293             Returns the salt, as an integer.
294              
295             =cut
296              
297             sub salt {
298 12     12 1 42 my($self) = @_;
299 12         71 return $self->{salt};
300             }
301              
302             =item $ppr->salt_hex
303              
304             Returns the salt, as a string of four hexadecimal digits. The first
305             two digits give the least-significant byte and the last two give the
306             most-significant byte, with most-significant nybble first within each
307             byte.
308              
309             =cut
310              
311             sub salt_hex {
312 19     19 1 49 my($self) = @_;
313 19         185 return sprintf("%02X%02X", $self->{salt} & 0xff, $self->{salt} >> 8);
314             }
315              
316             =item $ppr->hash
317              
318             Returns the hash value, as a string of eight bytes.
319              
320             =cut
321              
322             sub hash {
323 11     11 1 39 my($self) = @_;
324 11         75 return $self->{hash};
325             }
326              
327             =item $ppr->hash_hex
328              
329             Returns the hash value, as a string of 16 uppercase hexadecimal digits.
330              
331             =cut
332              
333             sub hash_hex {
334 23     23 1 65 my($self) = @_;
335 23         218 return uc(unpack("H*", $self->{hash}));
336             }
337              
338             =item $ppr->match(PASSPHRASE)
339              
340             =item $ppr->as_crypt
341              
342             =item $ppr->as_rfc2307
343              
344             These methods are part of the standard L interface.
345              
346             =cut
347              
348             sub _passphrase_acceptable {
349 26     26   64 my($self, $passphrase) = @_;
350 26         969 return $passphrase =~ /\A[_\$0-9A-Za-z]{1,32}\z/;
351             }
352              
353             my %hpwd_alg_num = (
354             PURDY => UAI_C_PURDY,
355             PURDY_V => UAI_C_PURDY_V,
356             PURDY_S => UAI_C_PURDY_S,
357             );
358              
359             sub _hash_of {
360 22     22   52 my($self, $passphrase) = @_;
361             return lgi_hpwd($self->{username}, uc($passphrase),
362 22         278 $hpwd_alg_num{$self->{algorithm}}, $self->{salt});
363             }
364              
365             sub match {
366 17     17 1 6359 my($self, $passphrase) = @_;
367             return $self->_passphrase_acceptable($passphrase) &&
368 17   66     50 $self->_hash_of($passphrase) eq $self->{hash};
369             }
370              
371             my %crypt_alg_num = (
372             PURDY => "1",
373             PURDY_V => "2",
374             PURDY_S => "3",
375             );
376              
377             sub as_crypt {
378 12     12 1 38 my($self) = @_;
379             return "\$VMS".$crypt_alg_num{$self->{algorithm}}."\$".
380 12         51 $self->salt_hex.$self->hash_hex.$self->{username};
381             }
382              
383             =back
384              
385             =head1 SEE ALSO
386              
387             L,
388             L
389              
390             =head1 AUTHOR
391              
392             Andrew Main (Zefram)
393              
394             =head1 COPYRIGHT
395              
396             Copyright (C) 2006, 2007, 2009, 2010, 2012
397             Andrew Main (Zefram)
398              
399             =head1 LICENSE
400              
401             This module is free software; you can redistribute it and/or modify it
402             under the same terms as Perl itself.
403              
404             =cut
405              
406             1;