File Coverage

blib/lib/Crypt/HSXKPasswd.pm
Criterion Covered Total %
statement 671 974 68.8
branch 163 326 50.0
condition 24 61 39.3
subroutine 69 85 81.1
pod 32 35 91.4
total 959 1481 64.7


line stmt bran cond sub pod time code
1             package Crypt::HSXKPasswd;
2              
3             # import required modules
4 3     3   316576 use strict;
  3         8  
  3         102  
5 3     3   14 use warnings;
  3         5  
  3         96  
6 3     3   13 use Carp; # for nicer 'exception' handling for users of the module
  3         10  
  3         239  
7 3     3   4948 use Fatal qw( :void open close binmode ); # make builtins throw exceptions on failure
  3         39486  
  3         18  
8 3     3   6559 use English qw( -no_match_vars ); # for more readable code
  3         10233  
  3         17  
9 3     3   1075 use Scalar::Util qw( blessed ); # for checking if a reference is blessed
  3         3  
  3         195  
10 3     3   2545 use Math::Round; # for round()
  3         16002  
  3         277  
11 3     3   5085 use Math::BigInt; # for the massive numbers needed to store the permutations
  3         66389  
  3         19  
12 3     3   62366 use Clone qw( clone ); # for cloning nested data structures - exports clone()
  3         18155  
  3         265  
13 3     3   20601 use Readonly; # for truly constant constants
  3         7658  
  3         180  
14 3     3   5296 use JSON; # for dealing with JSON strings
  3         79804  
  3         18  
15 3     3   26556 use List::MoreUtils qw( uniq ); # for array deduplication
  3         63568  
  3         25  
16 3     3   7975 use Type::Tiny; # for generating anonymous type constraints when needed
  3         104038  
  3         141  
17 3     3   3173 use Type::Params qw( compile multisig ); # for parameter validation with Type::Tiny objects
  3         196813  
  3         29  
18 3     3   743 use Types::Standard qw( slurpy :types ); # for basic type checking (Int Str etc.)
  3         5  
  3         16  
19 3     3   11419 use Crypt::HSXKPasswd::Types qw( :types ); # for custom type checking
  3         12  
  3         48  
20 3     3   8470 use Crypt::HSXKPasswd::Helper; # exports utility functions like _error & _warn
  3         51  
  3         262  
21 3     3   1603 use Crypt::HSXKPasswd::Dictionary::Basic;
  3         10  
  3         137  
22 3     3   1590 use Crypt::HSXKPasswd::RNG::Math_Random_Secure;
  3         10  
  3         129  
23 3     3   10964 use Crypt::HSXKPasswd::RNG::Data_Entropy;
  3         13  
  3         376  
24 3     3   3078 use Crypt::HSXKPasswd::RNG::DevUrandom;
  3         11  
  3         137  
25 3     3   1848 use Crypt::HSXKPasswd::RNG::Basic;
  3         8  
  3         112  
26              
27             # set things up for using UTF-8
28 3     3   48 use 5.016; # min Perl for good UTF-8 support, implies feature 'unicode_strings'
  3         9  
29 3     3   14 use Encode qw( encode decode );
  3         8  
  3         153  
30 3     3   4102 use Text::Unidecode; # for stripping accents from accented characters
  3         3688  
  3         179  
31 3     3   17 use utf8;
  3         4  
  3         14  
32             binmode STDOUT, ':encoding(UTF-8)';
33              
34             # import (or not) optional modules
35             eval{
36             # the default dicrionary may not have been geneated using the Util module
37             require Crypt::HSXKPasswd::Dictionary::EN;
38             }or do{
39             carp('WARNING - failed to load Crypt::HSXKPasswd::Dictionary::EN');
40             };
41              
42             ## no critic (ProhibitAutomaticExportation);
43 3     3   256 use base qw( Exporter );
  3         6  
  3         492  
44             our @EXPORT = qw( hsxkpasswd );
45             ## use critic
46              
47             # Copyright (c) 2015, Bart Busschots T/A Bartificer Web Solutions All rights
48             # reserved.
49             #
50             # Code released under the FreeBSD license (included in the POD at the bottom of
51             # this file)
52              
53             #==============================================================================
54             # Code
55             #==============================================================================
56              
57             #
58             # === Constants and Package Variables =========================================#
59             #
60              
61             # version info
62 3     3   19 use version; our $VERSION = qv('3.6');
  3         5  
  3         16  
63              
64             # entropy control variables
65             my $_ENTROPY_MIN_BLIND = 78; # 78 bits - equivalent to 12 alpha numeric characters with mixed case and symbols
66             my $_ENTROPY_MIN_SEEN = 52; # 52 bits - equivalent to 8 alpha numeric characters with mixed case and symbols
67             my $_ENTROPY_WARNINGS = 'ALL'; # valid values are 'ALL', 'BLIND', or 'NONE' (invalid values treated like 'ALL')
68              
69             # utility constants
70             Readonly my $_CLASS => __PACKAGE__;
71             Readonly my $_TYPES_CLASS => 'Crypt::HSXKPasswd::Types';
72             Readonly my $_DICTIONARY_BASE_CLASS => 'Crypt::HSXKPasswd::Dictionary';
73             Readonly my $_RNG_BASE_CLASS => 'Crypt::HSXKPasswd::RNG';
74              
75             #
76             # Constructor -----------------------------------------------------------------
77             #
78              
79             #####-SUB-######################################################################
80             # Type : CONSTRUCTOR (CLASS)
81             # Purpose : Instantiate an object of type XKPasswd
82             # Returns : An object of type XKPasswd
83             # Arguments : This function accepts the following named arguments - all
84             # optional:
85             # dictionary - an object that inherits from
86             # Crypt::HSXKPasswd::Dictionary
87             # dictionary_list - an array ref of words to use as a dictionary.
88             # dictionary_file - the path to a dictionary file.
89             # dictionary_file_encoding - the encoding to use when loading the
90             # dictionary file, defaults to UTF-8.
91             # preset - the preset to use
92             # preset_overrides - a hashref of config options to override.
93             # Ignored unless preset is set, and in use.
94             # config - a config hashref.
95             # config_json - a config as a JSON string (requires that the JSON
96             # module be installed)
97             # rng - an object that inherits from Crypt::HSXKPasswd::RNG
98             # Throws : Croaks if the function is called in an invalid way, called with
99             # invalid args, or called with a JSON string when JSON is not
100             # installed.
101             # Notes : The order of preference for word sources is dictionary, then
102             # dictionary_list, then dictionary_file. If none are specified,
103             # then an instance of Crypt::HSXKPasswd::Dictionary::EN will be
104             # used.
105             # The order of preference for the configuration source is config
106             # then config_json, then preset. If no configuration source is
107             # specified, then the preset 'DEFAULT' is used.
108             # If no RNG is passed, _best_available_rng() will be used to
109             # instantiate and instance of the most secure RNG usable on the
110             # system.
111             # See Also : For valid configuarion options see POD documentation below
112             sub new{
113 10     10 0 85 my @args = @_;
114 10         45 my $class = shift @args;
115 10         47 _force_class($class);
116            
117             # validate args
118 10         23 state $args_check = compile(slurpy Dict[
119             dictionary => Optional[InstanceOf[$_DICTIONARY_BASE_CLASS]],
120             dictionary_list => Optional[ArrayRef[Str]],
121             dictionary_file => Optional[Str],
122             dictionary_file_encoding => Optional[Str],
123             config => Optional[Config],
124             config_json => Optional[Str],
125             preset => Optional[PresetName],
126             preset_overrides => Optional[ConfigOverride],
127             rng => Optional[InstanceOf[$_RNG_BASE_CLASS]],
128             ]);
129 10         10396 my ($options) = $args_check->(@args);
130            
131             # set defaults
132 10 100       875 $options->{dictionary_file_encoding} = 'UTF-8' unless $options->{dictionary_file_encoding};
133            
134            
135             # before going any further, check the presets and key definitions if debugging (doing later may cause an error before we test)
136 10 50       40 if($_CLASS->module_config('DEBUG')){
137 0         0 $_CLASS->_check_config_key_definitions();
138 0         0 $_CLASS->_check_preset_definitions();
139             }
140            
141             # process the word source
142 10         17 my $dictionary;
143 10 100       53 if($options->{dictionary}){
    100          
    100          
144 1         2 $dictionary = $options->{dictionary};
145             }elsif($options->{dictionary_list}){
146 1         11 $dictionary = Crypt::HSXKPasswd::Dictionary::Basic->new($options->{dictionary_list});
147             }elsif($options->{dictionary_file}){
148 2         21 $dictionary = Crypt::HSXKPasswd::Dictionary::Basic->new($options->{dictionary_file}, $options->{dictionary_file_encoding});
149             }else{
150 6         54 $dictionary = Crypt::HSXKPasswd::Dictionary::EN->new();
151             }
152            
153             # process the config source
154 10         18 my $config = {};
155 10 100       51 if($options->{config}){
    100          
    100          
156 1         2 $config = $options->{config};
157             }elsif($options->{config_json}){
158 1         2 $config = $options->{config_json}; # pass the string on, config() will deal with it
159             }elsif($options->{preset}){
160 2         7 $config = $_CLASS->preset_config($options->{preset}, $options->{preset_overrides});
161             }else{
162 6         35 $config = $_CLASS->default_config();
163             }
164            
165             # process the random number source
166 10         24 my $rng = {};
167 10 100       40 if($options->{rng}){
168 1         2 $rng = $options->{rng};
169             }else{
170 9         45 $rng = $_CLASS->_best_available_rng();
171             }
172            
173             # initialise the object
174 10         91 my $instance = {
175             # 'public' instance variables (none so far)
176             # 'PRIVATE' internal variables
177             _CONFIG => {},
178             _DICTIONARY_SOURCE => {}, # the dictionary object to source words from
179             _RNG => {}, # the random number generator
180             _CACHE_DICTIONARY_FULL => [], # a cache of all words found in the dictionary file
181             _CACHE_DICTIONARY_LIMITED => [], # a cache of all the words found in the dictionary file that meet the length criteria
182             _CACHE_CONTAINS_ACCENTS => 0, # a cache of whether or not the filtered word list contains words with accented letters
183             _CACHE_ENTROPYSTATS => {}, # a cache of the entropy stats for the current combination of dictionary and config
184             _CACHE_RANDOM => [], # a cache of random numbers (as floating points between 0 and 1)
185             _PASSWORD_COUNTER => 0, # the number of passwords this instance has generated
186             };
187 10         20 bless $instance, $class;
188            
189             # load the config - will croak on invalid config
190 10         35 $instance->config($config);
191            
192             # load the dictionary (can't be done until the config is loaded)
193 10         32 $instance->dictionary($dictionary);
194            
195             # load the rng
196 10         46 $instance->rng($rng);
197            
198             # if debugging, print status
199 10         40 _debug("instantiated $_CLASS object with the following details:\n".$instance->status());
200            
201             # return the initialised object
202 10         114 return $instance;
203             }
204              
205             #
206             # Public Class (Static) functions ---------------------------------------------
207             #
208              
209             #####-SUB-######################################################################
210             # Type : CLASS
211             # Purpose : Return or update a module config variable
212             # Returns : The value of the specified module config variable
213             # Arguments : 1) the name of the module config variable
214             # 2) OPTIONAL - a new value for the module config variable
215             # Throws : Croaks on invalid invocation, or invalid args
216             # Notes :
217             # See Also :
218             sub module_config{
219 11     11 1 221 my @args = @_;
220 11         46 my $class = shift @args;
221 11         36 _force_class($class);
222            
223             # validate args
224 11         19 state $args_check = compile(
225             NonEmptyString->plus_coercions(Str, q{uc $_}), ## no critic (RequireInterpolationOfMetachars)
226             Optional[Maybe[Value]],
227             );
228 11         2842 my ($config_key, $new_value) = $args_check->(@args);
229            
230            
231             # figure out which variable we are accessing
232             ## no critic (ProhibitCascadingIfElse);
233 11 50       164 if($config_key eq 'LOG_STREAM'){
    50          
    100          
    50          
    50          
    50          
234             # check if we are a setter
235 0 0       0 if(defined $new_value){
236             # make sure the new value is valid
237 0 0       0 FileHandle->check($new_value) || _error(FileHandle->get_message($new_value));
238            
239             # save the new value
240 0         0 $Crypt::HSXKPasswd::Helper::_LOG_STREAM = $new_value; ## no critic (ProtectPrivateVars)
241             }
242            
243             #return the value
244 0         0 return $Crypt::HSXKPasswd::Helper::_LOG_STREAM; ## no critic (ProtectPrivateVars)
245             }
246             elsif($config_key eq 'LOG_ERRORS'){
247             # check if we are a setter
248 0 0       0 if(defined $new_value){
249             # make sure the new value is valid
250 0 0       0 TrueFalse->check($new_value) || _error(TrueFalse->get_message($new_value));
251            
252             # save the new value
253 0         0 $Crypt::HSXKPasswd::Helper::_LOG_ERRORS = $new_value; ## no critic (ProtectPrivateVars)
254             }
255            
256             #return the value
257 0         0 return $Crypt::HSXKPasswd::Helper::_LOG_ERRORS; ## no critic (ProtectPrivateVars)
258             }
259             elsif($config_key eq 'DEBUG'){
260             # check if we are a setter
261 10 50       29 if(defined $new_value){
262             # make sure the new value is valid
263 0 0       0 TrueFalse->check($new_value) || _error(TrueFalse->get_message($new_value));
264            
265             # save the new value
266 0         0 $Crypt::HSXKPasswd::Helper::_DEBUG = $new_value; ## no critic (ProtectPrivateVars)
267             }
268            
269             #return the value
270 10         38 return $Crypt::HSXKPasswd::Helper::_DEBUG; ## no critic (ProtectPrivateVars)
271             }elsif($config_key eq 'ENTROPY_MIN_BLIND'){
272             # check if we are a setter
273 0 0       0 if(defined $new_value){
274             # make sure the new value is valid
275 0 0       0 PositiveInteger->check($new_value) || _error(PositiveInteger->get_message($new_value));
276            
277             # save the new value
278 0         0 $_ENTROPY_MIN_BLIND = $new_value;
279             }
280            
281             #return the value
282 0         0 return $_ENTROPY_MIN_BLIND;
283             }elsif($config_key eq 'ENTROPY_MIN_SEEN'){
284             # check if we are a setter
285 0 0       0 if(defined $new_value){
286             # make sure the new value is valid
287 0 0       0 PositiveInteger->check($new_value) || _error(PositiveInteger->get_message($new_value));
288            
289             # save the new value
290 0         0 $_ENTROPY_MIN_SEEN = $new_value;
291             }
292            
293             #return the value
294 0         0 return $_ENTROPY_MIN_SEEN;
295             }elsif($config_key eq 'ENTROPY_WARNINGS'){
296             # check if we are a setter
297 1 50       5 if(defined $new_value){
298             # make sure the new value is valid
299 1 50       4 EntropyWarningLevel->check($new_value) || _error(EntropyWarningLevel->get_message($new_value));
300            
301             # save the new value
302 1         6 $_ENTROPY_WARNINGS = $new_value;
303             }
304            
305             #return the value
306 1         3 return $_ENTROPY_WARNINGS;
307             }else{
308             # the config key was invalid
309 0         0 _error(qq{no package variable '$config_key'});
310             }
311             ## use critic
312            
313             # It's not possible to get here, so return 1 to keep PerlCritic happy
314 0         0 return 1;
315             }
316              
317             #####-SUB-######################################################################
318             # Type : CLASS
319             # Purpose : Get a list of defined config keys.
320             # Returns : An array of strings.
321             # Arguments : NONE
322             # Throws : NOTHING
323             # Notes :
324             # See Also :
325             sub defined_config_keys{
326             # gather and return the list of config key names
327 2     2 1 13 return (sort keys %{$_TYPES_CLASS->_config_keys()});
  2         5  
328             }
329              
330             #####-SUB-######################################################################
331             # Type : CLASS
332             # Purpose : Return the specification for a given config key.
333             # Returns : A hash indexed by 'required', 'type', and 'expects'.
334             # Arguments : 1) a valid config key name
335             # Throws : Croaks on invalid invocation and args
336             # Notes :
337             # See Also :
338             sub config_key_definition{
339 0     0 1 0 my @args = @_;
340 0         0 my $class = shift @args;
341 0         0 _force_class($class);
342            
343             # validate args
344 0         0 state $args_check = compile(ConfigKeyName);
345 0         0 my ($key) = $args_check->(@args);
346            
347             # get a referece to the keys hashref from the Types class
348 0         0 my $defined_keys = $_TYPES_CLASS->_config_keys();
349            
350             # assemble the hash
351             my %definition = (
352             required => $defined_keys->{$key}->{required},
353             type => $defined_keys->{$key}->{type},
354             expects => $defined_keys->{$key}->{expects},
355 0         0 );
356            
357             # return the hash
358 0         0 return %definition;
359             }
360              
361             #####-SUB-######################################################################
362             # Type : CLASS
363             # Purpose : Return a hash of all key definitions indexed by key name.
364             # Returns : A hash of key defintions as returned by config_key_definition().
365             # Arguments : NONE
366             # Throws : NOTHING
367             # Notes :
368             # See Also : config_key_definition()
369             sub config_key_definitions{
370             # gather the definitions
371 0     0 1 0 my %definitions = ();
372 0         0 foreach my $key ($_CLASS->defined_config_keys()){
373 0         0 $definitions{$key} = $_CLASS->config_key_definition($key);
374             }
375            
376             # return the definitions
377 0         0 return %definitions;
378             }
379              
380             #####-SUB-######################################################################
381             # Type : CLASS
382             # Purpose : generate a config hashref populated with the default values
383             # Returns : a hashref
384             # Arguments : 1. OPTIONAL - a hashref with config keys to over-ride when
385             # assembling the config
386             # Throws : Croaks if invoked in an invalid way. If passed overrides also
387             # Croaks if the resulting config is invalid, and Carps if passed
388             # on each invalid key passed in the overrides hashref.
389             # Notes :
390             # See Also :
391             sub default_config{
392 6     6 1 54 my @args = @_;
393 6         28 my $class = shift @args;
394 6         25 _force_class($class);
395            
396             # validate args
397 6         11 state $args_check = compile(Optional[ConfigOverride]);
398 6         1191 my ($overrides) = $args_check->(@args);
399              
400             # build and return a default config
401 6         97 return $_CLASS->preset_config('DEFAULT', $overrides);
402             }
403              
404             #####-SUB-######################################################################
405             # Type : CLASS
406             # Purpose : Return the specification for a given preset.
407             # Returns : A hash indexed by 'description', and 'config'.
408             # Arguments : 1) OPTIONAL - a valid preset name, defaults to 'DEFAULT'
409             # Throws : Croaks on invalid invocation and args
410             # Notes :
411             # See Also :
412             sub preset_definition{
413 0     0 1 0 my @args = @_;
414 0         0 my $class = shift @args;
415 0         0 _force_class($class);
416            
417             # validate args
418 0         0 state $args_check = compile(Optional[Maybe[PresetName]]);
419 0         0 my ($preset_name) = $args_check->(@args);
420            
421             # set defaults
422 0 0       0 $preset_name = 'DEFAULT' unless $preset_name;
423            
424             # get a referece to the presets hashref from the Types class
425 0         0 my $preset_defs = $_TYPES_CLASS->_presets();
426            
427             # assemble the hash
428             my %definition = (
429             description => $preset_defs->{$preset_name}->{description},
430             config => $preset_defs->{$preset_name}->{config},
431 0         0 );
432            
433             # return the hash
434 0         0 return %definition;
435             }
436              
437             #####-SUB-######################################################################
438             # Type : CLASS
439             # Purpose : Return a hash of all preset definitions indexed by name.
440             # Returns : A hash of preset defintions as returned by preset_definition().
441             # Arguments : NONE
442             # Throws : NOTHING
443             # Notes :
444             # See Also : preset_definition()
445             sub preset_definitions{
446             # gather the definitions
447 0     0 1 0 my %definitions = ();
448 0         0 foreach my $name ($_CLASS->defined_presets()){
449 0         0 $definitions{$name} = $_CLASS->preset_definition($name);
450             }
451            
452             # return the definitions
453 0         0 return %definitions;
454             }
455              
456             #####-SUB-######################################################################
457             # Type : CLASS
458             # Purpose : generate a config hashref populated using a preset
459             # Returns : a hashref
460             # Arguments : 1. OPTIONAL - The name of the preset to assemble the config for
461             # as a scalar. If no name is passed, the preset 'DEFAULT' is
462             # used
463             # 2. OPTIONAL - a hashref with config keys to over-ride when
464             # assembling the config
465             # Throws : Croaks if invoked in an invalid way. If passed overrides also
466             # Croaks if the resulting config is invalid, and Carps if passed
467             # on each invalid key passed in the overrides hashref.
468             # Notes :
469             # See Also :
470             sub preset_config{
471 10     10 1 59 my @args = @_;
472 10         40 my $class = shift @args;
473 10         35 _force_class($class);
474            
475             # validate args
476 10         18 state $args_check = compile(Optional[PresetName], Optional[Maybe[ConfigOverride]]);
477 10         2853 my ($preset_name, $overrides) = $args_check->(@args);
478            
479             # default the preset name to 'DEFAULT'
480 10 50       361 $preset_name = 'DEFAULT' unless $preset_name;
481            
482             # get a reference to the Presets hashref from the Types class
483 10         35 my $preset_defs = $_TYPES_CLASS->_presets();
484            
485             # start by loading the preset
486 10         33 my $config = $_CLASS->clone_config($preset_defs->{$preset_name}->{config});
487            
488             # if overrides were passed, apply them and validate
489 10 100       32 if(defined $overrides){
490             # save the keys into the config
491 1         1 foreach my $key (keys %{$overrides}){
  1         3  
492 1         3 $config->{$key} = $overrides->{$key};
493             }
494             # validate the resulting config
495 1 50       5 unless(Config->check($config)){
496 0         0 _error('The preset combined with the specified overrides produces an invalid config: '.Config->get_message($config));
497             }
498             }
499            
500             # return the config
501 10         55 return $config;
502             }
503              
504             #####-SUB-######################################################################
505             # Type : CLASS
506             # Purpose : Resturn the presets defined in the Crypt::HSXKPasswd module as a
507             # JSON string
508             # Returns : A JSON String as a scalar. The JSON string represets a hashref
509             # with three keys - 'defined_keys' contains an array of preset
510             # identifiers, 'presets' contains the preset configs indexed by
511             # preset identifier, and 'preset_descriptions' contains the a
512             # hashref of descriptions indexed by preset identifiers
513             # Arguments : NONE
514             # Throws : If there is a problem converting the objects to JSON.
515             # Notes :
516             # See Also :
517             sub presets_json{
518             # assemble an object containing the presets with any keys that can't be
519             # converted to JSON removed
520 0     0 1 0 my @defined_presets = $_CLASS->defined_presets();
521 0         0 my $sanitised_presets = {};
522 0         0 my $preset_descriptions = {};
523 0         0 foreach my $preset_name (@defined_presets){
524 0         0 $sanitised_presets->{$preset_name} = $_CLASS->preset_config($preset_name);
525 0         0 $preset_descriptions->{$preset_name} = $_CLASS->preset_description($preset_name);
526             }
527 0         0 my $return_object = {
528             defined_presets => [@defined_presets],
529             presets => $sanitised_presets,
530             preset_descriptions => $preset_descriptions,
531             };
532            
533             # try convert the object to a JSON string
534 0         0 my $json_string = q{};
535             eval{
536 0         0 $json_string = JSON->new()->encode($return_object);
537 0         0 1; # ensure truthy evaluation on succesful execution
538 0 0       0 }or do{
539 0         0 _error("failed to render presets as JSON string with error: $EVAL_ERROR");
540             };
541            
542             # return the JSON string
543 0         0 return $json_string;
544             }
545              
546             #####-SUB-######################################################################
547             # Type : CLASS
548             # Purpose : Clone a config hashref
549             # Returns : a hashref
550             # Arguments : 1. the config hashref to clone
551             # Throws : Croaks if called in an invalid way, or with an invalid config.
552             # Notes : This function needs to be updated each time a new non-scalar
553             # valid config key is added to the library.
554             # See Also :
555             sub clone_config{
556 20     20 1 141 my @args = @_;
557 20         78 my $class = shift @args;
558 20         64 _force_class($class);
559            
560             # validate args
561 20         34 state $args_check = compile(Config);
562 20         778 my ($config) = $args_check->(@args);
563            
564 20         777 return clone($config);
565             }
566              
567             #####-SUB-######################################################################
568             # Type : CLASS
569             # Purpose : Remove all keys from a hashref that are not valid config keys
570             # Returns : A reference to a hashref
571             # Arguments : 1) a hashref
572             # 2) OPTIONAL - a list of named arguments:
573             # 'warn_invalid_key_names' - must be either 1 or 0. If 1,
574             # warnings will be issued for any keys containined in the
575             # test hash that are not valid config keys.
576             # 'suppress_warnings' takes precedence over this argument.
577             # 'suppress_warnings' - must be either 1 or 0. If 1, no warnings
578             # will be printed when dropping valid keys with invalid
579             # values, or invalid keys.
580             # Throws : Croaks on invalid args. Unless configured not to, will warn if
581             # a valid key with an invalid value is encountered.
582             # Notes :
583             # See Also :
584             sub distil_to_config_keys{
585 1     1 1 8 my @args = @_;
586 1         4 my $class = shift @args;
587 1         3 _force_class($class);
588            
589             # validate args
590 1         3 state $args_check = compile(HashRef,
591             slurpy Dict[
592             suppress_warnings => Optional[TrueFalse],
593             warn_invalid_key_names => Optional[TrueFalse],
594             ],
595             );
596 1         6989 my ($hashref, $options) = $args_check->(@args);
597            
598             # get a list of all the defined keys
599 1         137 my @defined_keys = $_CLASS->defined_config_keys();
600            
601             # if warnings are not suppressed, and if extra warnings are asked for, check for invalid keys
602 1 50 33     13 if(!$options->{suppress_warnings} && $options->{warn_invalid_key_names}){
603             # build a lookup table to quickly test if a key exists
604 0         0 my %defined_keys_lookup = ();
605 0         0 foreach my $key (@defined_keys){
606 0         0 $defined_keys_lookup{$key} = 1;
607             }
608            
609             # check each key in the test hash against the lookup table
610 0         0 foreach my $test_key (sort keys %{$hashref}){
  0         0  
611 0 0       0 unless($defined_keys_lookup{$test_key}){
612 0         0 _warn(qq{distilling out undefined config key '$test_key'});
613             }
614             }
615             }
616            
617             # start with a new blank hashref, and copy across only the valid keys
618 1         1 my $distilled = {};
619 1         3 foreach my $key (@defined_keys){
620 17 100       30 if(defined $hashref->{$key}){
621 9 50       16 if(ConfigKeyAssignment->check({$key => $hashref->{$key}})){
622 9         90 $distilled->{$key} = clone($hashref->{$key});
623             }else{
624 0 0       0 _warn("distilling out valid config key '$key' because of invalid value: ".ConfigKeyAssignment->get_message({$key => $hashref->{$key}})) unless $options->{suppress_warnings};
625             }
626             }
627             }
628 1         2 _debug('hashref distilled down from '.(scalar keys %{$hashref}).' to '.(scalar keys %{$distilled}).' keys');
  1         5  
  1         6  
629            
630             # return the distilled hashref
631 1         4 return $distilled;
632             }
633              
634             #####-SUB-######################################################################
635             # Type : CLASS
636             # Purpose : Distil an array of strings down to a de-duplicated array of only
637             # symbols.
638             # Returns : An array of strings
639             # Arguments : 1) A reference to an array of strings
640             # 2) OPTIONAL - a named argument warn with a value of 0 or 1. If 1
641             # is passed, warnings will be issued each time an invalid string
642             # is skipped over.
643             # Throws : Croaks on invalid invocation or args, and warns on request when
644             # skipping words.
645             # Notes :
646             # See Also :
647             sub distil_to_symbol_alphabet{
648 6     6 1 53 my @args = @_;
649 6         30 my $class = shift @args;
650 6         29 _force_class($class);
651            
652             # validate args
653 6         15 state $args_check = compile(ArrayRef[Str], slurpy Dict[warn => Optional[TrueFalse]]);
654 6         6515 my ($array_ref, $options) = $args_check->(@args);
655 6   50     357 my $warn = $options->{warn} || 0;
656            
657             # loop through the array and copy all valid synbols to a new array
658 6         13 my @valid_symbols = ();
659 6         8 foreach my $potential_symbol (@{$array_ref}){
  6         16  
660 108 50       180 if(Symbol->check($potential_symbol)){
661 108         476 push @valid_symbols, $potential_symbol;
662             }else{
663 0 0 0     0 if($warn || _do_debug()){
664 0         0 my $msg = 'skipping invalid symbol: '.Symbol->get_message($potential_symbol);
665 0 0       0 if($warn){
666 0         0 _warn($msg);
667             }else{
668 0         0 _debug($msg);
669             }
670             }
671             }
672             }
673            
674             # de-dupe the valid symbols
675 6         94 my @final_alphabet = uniq(@valid_symbols);
676            
677             # return the valid symbols
678 6         64 return @final_alphabet;
679             }
680              
681             #####-SUB-######################################################################
682             # Type : CLASS
683             # Purpose : Distil an array of strings down to a de-duplicated array of only
684             # the valid words.
685             # Returns : An array of words
686             # Arguments : 1) A reference to an array of strings
687             # 2) OPTIONAL - a named argument warn with a value of 0 or 1. If 1
688             # is passed, warnings will be issued each time an invalid string
689             # is skipped over.
690             # Throws : Croaks on invalid invocation or args, and warns on request when
691             # skipping words.
692             # Notes :
693             # See Also :
694             sub distil_to_words{
695 10     10 1 77 my @args = @_;
696 10         40 my $class = shift @args;
697 10         37 _force_class($class);
698            
699             # just pass everything through to the dictionary class
700 10         27 return $_DICTIONARY_BASE_CLASS->distil_to_words(@args);
701             }
702              
703             #####-SUB-######################################################################
704             # Type : CLASS
705             # Purpose : validate a config hashref
706             # Returns : 1 if the config is valid, 0 otherwise
707             # Arguments : 1. a hashref to validate
708             # 2. OPTIONAL - a named argument croak with a value of 1 or 1
709             # Throws : Croaks on invalid args, or on error if second arg is truthy
710             # Notes : This function needs to be updated each time a new valid config
711             # key is added to the library.
712             # See Also :
713             sub is_valid_config{
714 0     0 1 0 my @args = @_;
715 0         0 my $class = shift @args;
716 0         0 _force_class($class);
717            
718             # validate args
719 0         0 state $args_check = compile(Item, slurpy Dict[croak => Optional[TrueFalse]]);
720 0         0 my ($config, $options) = $args_check->(@args);
721            
722             # validate the config
723 0   0     0 my $is_valid = Config->check($config) || 0;
724            
725             # croak if appropriate
726 0 0 0     0 if(!$is_valid && $options->{croak}){
727 0         0 _error(Config->get_message($config));
728             }
729            
730             # return the result of the validation
731 0         0 return $is_valid;
732             }
733              
734             #####-SUB-######################################################################
735             # Type : CLASS
736             # Purpose : Convert a config hashref to a JSON String
737             # Returns : A scalar
738             # Arguments : 1. A config hashref
739             # Throws : Croaks on invalid invocation, invalid args, or if the JSON module
740             # is not available
741             # Notes :
742             # See Also :
743             sub config_to_json{
744 1     1 1 3 my @args = @_;
745 1         2 my $class = shift @args;
746 1         4 _force_class($class);
747            
748             # validate args
749 1         5 state $args_check = compile(Config);
750 1         960 my ($config) = $args_check->(@args);
751            
752             # try render the config to a JSON string
753 1         9 my $ans = q{};
754             eval{
755 1         19 $ans = encode_json($config);
756 1         5 1; # ensure a thurthy evaluation on successful execution
757 1 50       2 }or do{
758 0         0 _error("Failed to convert config to JSON stirng with error: $EVAL_ERROR");
759             };
760            
761             # return the string
762 1         5 return $ans;
763             }
764              
765             #####-SUB-######################################################################
766             # Type : CLASS
767             # Purpose : Convert a config hashref to a String
768             # Returns : A scalar
769             # Arguments : 1. A config hashref
770             # Throws : Croaks on invalid invocation or with invalid args. Carps if there
771             # are problems with the config hashref.
772             # Notes :
773             # See Also :
774             sub config_to_string{
775 10     10 1 80 my @args = @_;
776 10         37 my $class = shift @args;
777 10         30 _force_class($class);
778            
779             # validate args
780 10         21 state $args_check = compile(Config);
781 10         916 my ($config) = $args_check->(@args);
782            
783             # get a reference to the key definitions from the types class
784 10         174 my $defined_keys = $_TYPES_CLASS->_config_keys();
785            
786             # assemble the string to return
787 10         19 my $ans = q{};
788             CONFIG_KEY:
789 10         17 foreach my $key (sort keys %{$defined_keys}){
  10         100  
790             # skip undefined keys
791 170 100       278 next CONFIG_KEY unless defined $config->{$key};
792            
793             # process the key
794 114 100       144 if(ref $config->{$key} eq q{}){
    50          
    0          
795             # the key is a scalar
796 108         212 $ans .= $key.q{: '}.$config->{$key}.qq{'\n};
797             }elsif(ref $config->{$key} eq 'ARRAY'){
798             # the key is an array ref
799 6         13 $ans .= "$key: [";
800 6         12 my @parts = ();
801 6         8 foreach my $subval (sort @{$config->{$key}}){
  6         29  
802 108         127 push @parts, "'$subval'";
803             }
804 6         24 $ans .= join q{, }, @parts;
805 6         19 $ans .= "]\n";
806             }elsif(ref $config->{$key} eq 'HASH'){
807 0         0 $ans .= "$key: {";
808 0         0 my @parts = ();
809 0         0 foreach my $subkey (sort keys %{$config->{$key}}){
  0         0  
810 0         0 push @parts, "$subkey: '$config->{$key}->{$subkey}'";
811             }
812 0         0 $ans .= join q{, }, @parts;
813 0         0 $ans .= "}\n";
814             }else{
815             # this should never happen, but just in case, throw a warning
816 0         0 _warn("the data for the key '$key' is of an un-expected type (".(ref $config->{$key}).') - skipping key');
817             }
818             }
819            
820             # return the string
821 10         50 return $ans;
822             }
823              
824             #####-SUB-######################################################################
825             # Type : CLASS
826             # Purpose : Return the description for a given preset
827             # Returns : A scalar string
828             # Arguments : 1. OPTIONAL - the name of the preset to get the description for,
829             # if no name is passed 'DEFAULT' is assumed
830             # Throws : Croaks on invalid invocation or invalid args
831             # Notes :
832             # See Also :
833             sub preset_description{
834 0     0 1 0 my @args = @_;
835 0         0 my $class = shift @args;
836 0         0 _force_class($class);
837            
838             # validate args
839 0         0 state $args_check = compile(Optional[Maybe[PresetName]]);
840 0         0 my ($preset) = $args_check->(@args);
841            
842             # set defaults
843 0 0       0 $preset = 'DEFAULT' unless $preset;
844            
845             # get a reference to the preset definitions from the Types class
846 0         0 my $preset_defs = $_TYPES_CLASS->_presets();
847            
848             # return the description by loading the preset
849 0         0 return $preset_defs->{$preset}->{description};
850             }
851              
852              
853             #####-SUB-######################################################################
854             # Type : CLASS
855             # Purpose : Return a list of all valid preset names
856             # Returns : An array of preset names as scalars
857             # Arguments : NONE
858             # Throws : NOTHING
859             # Notes :
860             # See Also :
861             sub defined_presets{
862             # return the preset names
863 0     0 1 0 my @preset_names = sort keys %{$_TYPES_CLASS->_presets()};
  0         0  
864 0         0 return @preset_names;
865             }
866              
867             #####-SUB-######################################################################
868             # Type : CLASS
869             # Purpose : Render the defined presets as a string
870             # Returns : A scalar
871             # Arguments : NONE
872             # Throws : NOTHING
873             # Notes :
874             # See Also :
875             sub presets_to_string{
876             # get a reference to the preset definitions from th Types class
877 0     0 1 0 my $preset_defs = $_TYPES_CLASS->_presets();
878            
879             # loop through each preset and assemble the result
880 0         0 my $ans = q{};
881 0         0 my @preset_names = $_CLASS->defined_presets();
882 0         0 foreach my $preset (@preset_names){
883 0         0 $ans .= $preset."\n===\n";
884 0         0 $ans .= $preset_defs->{$preset}->{description}."\n";
885 0         0 $ans .= "\nConfig:\n---\n";
886 0         0 $ans .= $_CLASS->config_to_string($preset_defs->{$preset}->{config});
887 0         0 $ans .= "\nStatistics:\n---\n";
888 0         0 my %stats = $_CLASS->config_stats($preset_defs->{$preset}->{config});
889 0 0       0 if($stats{length_min} == $stats{length_max}){
890 0         0 $ans .= "Length (fixed): $stats{length_min} characters\n";
891             }else{
892 0         0 $ans .= "Length (variable): between $stats{length_min} & $stats{length_max} characters\n";
893             }
894 0         0 $ans .= "Random Numbers Needed Per-Password: $stats{random_numbers_required}\n";
895 0         0 $ans .= "\n";
896             }
897            
898             # return the string
899 0         0 return $ans;
900             }
901              
902             #####-SUB-#####################################################################
903             # Type : CLASS
904             # Purpose : Calculate the number of random numbers needed to genereate a
905             # single password with a given config.
906             # Returns : An integer
907             # Arguments : 1) a valid config hashref
908             # Throws : Croaks in invalid invocation, or invalid args
909             # Notes :
910             # See Also :
911             sub config_random_numbers_required{
912 35     35 0 272 my @args = @_;
913 35         155 my $class = shift @args;
914 35         148 _force_class($class);
915            
916             # validate args
917 35         59 state $args_check = compile(Config);
918 35         959 my ($config) = $args_check->(@args);
919            
920             # calculate the number of random numbers needed to generate the password
921 35         348 my $num_rand = 0;
922 35         63 $num_rand += $config->{num_words};
923 35 100       126 if($config->{case_transform} eq 'RANDOM'){
924 12         29 $num_rand += $config->{num_words};
925             }
926 35 100       99 if($config->{separator_character} eq 'RANDOM'){
927 23         50 $num_rand++;
928             }
929 35 100 66     177 if(defined $config->{padding_character} && $config->{padding_character} eq 'RANDOM'){
930 23         33 $num_rand++;
931             }
932 35         59 $num_rand += $config->{padding_digits_before};
933 35         55 $num_rand += $config->{padding_digits_after};
934            
935             # return the number
936 35         161 return $num_rand;
937             }
938              
939             #####-SUB-######################################################################
940             # Type : CLASS
941             # Purpose : Calculate statistics for a given configutration hashref.
942             # Returns : A hash of statistics indexed by the following keys:
943             # * 'length_min' - the minimum possible length of a password
944             # generated by the given config
945             # * 'length_max' - the maximum possible length of a password
946             # generated by the given config
947             # * 'random_numbers_required' - the number of random numbers needed
948             # to generate a single password using the given config
949             # Arguments : 1. A valid config hashref
950             # 2. OPTONAL - a named argument 'suppress_warnings' to indicate that
951             # no warnings should be issued if the config is such that there
952             # are uncertainties in the calculation.
953             # Throws : Croaks on invalid invocation or args, carps if multi-character
954             # substitutions are in use when not using adapive padding
955             # Notes : This function ignores character replacements, if one or more
956             # multi-character replacements are used when padding is not set
957             # to adaptive, this function will return an invalid max length.
958             # See Also :
959             sub config_stats{
960 20     20 1 132 my @args = @_;
961 20         87 my $class = shift @args;
962 20         64 _force_class($class);
963            
964             # validate args
965 20         42 state $args_check = compile(Config, slurpy Dict[suppress_warnings => Optional[TrueFalse]]);
966 20         5912 my ($config, $options) = $args_check->(@args);
967 20   100     753 my $suppres_warnings = $options->{suppress_warnings} || 0;
968            
969             # calculate the lengths
970 20         37 my $len_min = 0;
971 20         39 my $len_max = 0;
972 20 50       66 if($config->{padding_type} eq 'ADAPTIVE'){
973 0         0 $len_min = $len_max = $config->{pad_to_length};
974             }else{
975             # calcualte the length of everything but the words themselves
976 20         34 my $len_base = 0;
977 20 100       68 if($config->{padding_type} eq 'FIXED'){
978 12         20 $len_base += $config->{padding_characters_before};
979 12         22 $len_base += $config->{padding_characters_after};
980             }
981 20 100       67 if($config->{padding_digits_before} > 0){
982 12         18 $len_base += $config->{padding_digits_before};
983 12 50       34 if($config->{separator_character} ne 'NONE'){
984 12         29 $len_base++;
985             }
986             }
987 20 100       70 if($config->{padding_digits_after} > 0){
988 12         10 $len_base += $config->{padding_digits_after};
989 12 50       29 if($config->{separator_character} ne 'NONE'){
990 12         17 $len_base++;
991             }
992             }
993 20 50       52 if($config->{separator_character} ne 'NONE'){
994 20         47 $len_base += $config->{num_words} - 1;
995             }
996            
997             # maximise and minimise the word lengths to calculate the final answers
998 20         48 $len_min = $len_base + ($config->{num_words} * $config->{word_length_min});
999 20         47 $len_max = $len_base + ($config->{num_words} * $config->{word_length_max});
1000             }
1001            
1002             # calculate the number of random numbers needed to generate the password
1003 20         120 my $num_rand = $_CLASS->config_random_numbers_required($config);
1004            
1005             # detect whether or not we need to carp about multi-character replacements
1006 20 100 66     122 if($config->{padding_type} ne 'ADAPTIVE' && !$suppres_warnings){
1007 10 50       34 if(defined $config->{character_substitutions}){
1008             CHAR_SUB:
1009 0         0 foreach my $char (keys %{$config->{character_substitutions}}){
  0         0  
1010 0 0       0 if(length $config->{character_substitutions}->{$char} > 1){
1011 0         0 _warn('maximum length may be underestimated. The loaded config contains at least one character substitution which replaces a single character with multiple characters.');
1012 0         0 last CHAR_SUB;
1013             }
1014             }
1015             }
1016             }
1017            
1018             # assemble the result and return
1019 20         79 my $stats = {
1020             length_min => $len_min,
1021             length_max => $len_max,
1022             random_numbers_required => $num_rand,
1023             };
1024 20         32 return %{$stats};
  20         128  
1025             }
1026              
1027             #
1028             # --- Public Instance functions -----------------------------------------------
1029             #
1030              
1031             #####-SUB-#####################################################################
1032             # Type : INSTANCE
1033             # Purpose : Get the currently loaded dictionary object, or, load a new
1034             # dictionary
1035             # Returns : An clone of the loaded dictionary object, or, a reference to the
1036             # instance (to enable function chaining) if called as a setter
1037             # Arguments : 1. OPTIONAL - the source for the dictionary, can be:
1038             # The path to a dictionary file
1039             # -OR-
1040             # A reference to an array of words
1041             # -OR-
1042             # An instance of a sub-class of Crypt::HSXKPasswd::Dictionary
1043             # 2. OPTIONAL - the encoding to import the file with. The default
1044             # is UTF-8 (ignored if the first argument is not a file path).
1045             # Throws : Croaks on invalid invocation, or, if there is a problem loading
1046             # a dictionary file. While debugging, also warns when skipping
1047             # invalid words.
1048             # Notes :
1049             # See Also : For description of dictionary file format, see POD documentation
1050             # below
1051             sub dictionary{
1052 10     10 1 24 my @args = @_;
1053 10         15 my $self = shift @args;
1054 10         31 _force_instance($self);
1055            
1056             # validate args
1057 10         21 state $args_check = multisig(
1058             [],
1059             [NonEmptyString, Optional[Maybe[NonEmptyString]]],
1060             [InstanceOf[$_DICTIONARY_BASE_CLASS]],
1061             [ArrayRef[Str]],
1062             );
1063 10         3805 my ($dictionary_source, $encoding) = $args_check->(@args);
1064            
1065             # set defaults
1066 10 50       1403 $encoding = 'UTF-8' unless $encoding;
1067            
1068             # if we're a getter, just get and return
1069 10 50       32 unless(defined $dictionary_source){
1070 0         0 return $self->{_DICTIONARY_SOURCE}->clone();
1071             }
1072            
1073             # OK, so we're a setter - carry on!
1074            
1075             # croak if we are called before the config has been loaded into the instance
1076 10 50 33     61 unless(defined $self->{_CONFIG}->{word_length_min} && $self->{_CONFIG}->{word_length_max}){
1077 0         0 _error('failed to load word source - config has not been loaded yet');
1078             }
1079            
1080             # get a dictionary instance
1081 10         13 my $new_dict;
1082 10 50 33     104 if(blessed($dictionary_source) && $dictionary_source->isa($_DICTIONARY_BASE_CLASS)){
    0 0        
1083 10         85 $new_dict = $dictionary_source;
1084             }elsif(ref $dictionary_source eq q{} || ref $dictionary_source eq 'ARRAY'){
1085 0         0 $new_dict = Crypt::HSXKPasswd::Dictionary::Basic->new($dictionary_source, $encoding); # could throw an error
1086             }else{
1087 0         0 _error('invalid word source - must be a dictionary object, hashref, or file path');
1088             }
1089            
1090             # load and sanitise the words from the word source
1091 10         26 my @cache_full = $_CLASS->distil_to_words(\@{$new_dict->word_list()});
  10         53  
1092              
1093             # generate the cache of appropriate-length words - croaks if too few words left after filtering
1094             my @cache_limited = $_CLASS->_filter_word_list(
1095             \@cache_full,
1096             $self->{_CONFIG}->{word_length_min},
1097             $self->{_CONFIG}->{word_length_max},
1098             allow_accents => $self->{_CONFIG}->{allow_accents},
1099 10         952 );
1100              
1101             # if we got here all is well, so save the new path and caches into the object
1102 10         425 $self->{_DICTIONARY_SOURCE} = $new_dict;
1103 10         1476 $self->{_CACHE_DICTIONARY_FULL} = [@cache_full];
1104 10         1327 $self->{_CACHE_DICTIONARY_LIMITED} = [@cache_limited];
1105 10 50       46 if($self->{_CONFIG}->{allow_accents}){
1106 0         0 $self->{_CACHE_CONTAINS_ACCENTS} = $_CLASS->_contains_accented_letters(\@cache_limited);
1107             }else{
1108 10         22 $self->{_CACHE_CONTAINS_ACCENTS} = 0;
1109             }
1110            
1111             # update the instance's entropy cache
1112 10         52 $self->_update_entropystats_cache();
1113            
1114             # return a reference to self
1115 10         1495 return $self;
1116             }
1117              
1118             #####-SUB-######################################################################
1119             # Type : INSTANCE
1120             # Purpose : Get a clone of the current config from an instance, or load a
1121             # new config into the instance.
1122             # Returns : A config hashref if called with no arguments, or, the instance
1123             # if called with a hashref (to facilitate function chaining)
1124             # Arguments : 1. OPTIONAL - a configuration to load as:
1125             # A config hashref
1126             # -OR-
1127             # A JSON string representing a config hashref
1128             # Throws : Croaks if the function is called in an invalid way, with invalid
1129             # arguments, or with an invalid config.
1130             # Notes : Passing a JSON string will cause the function to croak if perl's
1131             # JSON module is not installed.
1132             # See Also : For valid configuarion options see POD documentation below
1133             sub config{
1134 10     10 1 21 my @args = @_;
1135 10         15 my $self = shift @args;
1136 10         29 _force_instance($self);
1137            
1138             # validate args
1139 10         17 state $args_check = multisig(
1140             [],
1141             [Config],
1142             [NonEmptyString],
1143             );
1144 10         2536 my ($config_raw) = $args_check->(@args);
1145            
1146             # if we're a getter, just get and return
1147 10 50       171 unless(defined $config_raw){
1148 0         0 return $self->_clone_config();
1149             }
1150            
1151             # OK - so we're a setter - carry on!
1152            
1153             # see what kind of argument we were passed, and behave appropriately
1154 10         21 my $config = {};
1155 10 100       30 if(ref $config_raw eq 'HASH'){
    50          
1156             # we received a hashref, so just and pass it on
1157 9         13 $config = $config_raw;
1158             }elsif(ref $config_raw eq q{}){
1159             # we received as string, so treat it as JSON
1160            
1161             # try parse the received string as JSON
1162 1         2 my $config_from_json = {};
1163             eval{
1164 1         12 $config_from_json = decode_json($config_raw);
1165 1         4 1; # ensure truthy evaluation on successful execution
1166 1 50       2 }or do{
1167 0         0 _error("Failed to parse JSON config string with error: $EVAL_ERROR");
1168             };
1169            
1170             # strip out any extraneous keys found
1171 1         4 $config = $_CLASS->distil_to_config_keys($config_from_json);
1172            
1173             # validate the generated config
1174 1 50       7 unless(Config->check($config)){
1175 0         0 _error('Config extracted from JSON string is not valid: '.Config->get_message($config));
1176             }
1177             }else{
1178 0         0 _error('the config passed must be a hashref or a JSON string');
1179             }
1180            
1181             # distil the alphabets in the new config
1182 10         62 $_CLASS->_distil_alphabets_inplace($config);
1183            
1184             # save a clone of the passed config into the instance
1185 10         42 $self->{_CONFIG} = $_CLASS->clone_config($config);
1186            
1187             # update the instance's entropy cache
1188 10         56 $self->_update_entropystats_cache();
1189            
1190             # return a reference to self to facilitate function chaining
1191 10         30 return $self;
1192             }
1193              
1194             #####-SUB-#####################################################################
1195             # Type : INSTANCE
1196             # Purpose : Return the config of the currently running instance as a JSON
1197             # string.
1198             # Returns : A scalar.
1199             # Arguments : NONE
1200             # Throws : Croaks if invoked in an invalid way. Carps if it meets a key of a
1201             # type not accounted for in the code.
1202             # Notes : This function will carp if the JSON module is not available
1203             # See Also :
1204             sub config_as_json{
1205 0     0 1 0 my $self = shift;
1206 0         0 _force_instance($self);
1207            
1208             # assemble and return the JSON string
1209 0         0 return $_CLASS->config_to_json($self->{_CONFIG}); # will croak without JSON
1210             }
1211              
1212             #####-SUB-#####################################################################
1213             # Type : INSTANCE
1214             # Purpose : Return the config of the currently running instance as a string.
1215             # Returns : A scalar.
1216             # Arguments : NONE
1217             # Throws : Croaks if invoked in an invalid way. Carps if it meets a key of a
1218             # type not accounted for in the code.
1219             # Notes :
1220             # See Also :
1221             sub config_as_string{
1222 10     10 1 17 my $self = shift;
1223 10         29 _force_instance($self);
1224            
1225             # assemble and return the string
1226 10         55 return $_CLASS->config_to_string($self->{_CONFIG});
1227             }
1228              
1229             #####-SUB-######################################################################
1230             # Type : INSTANCE
1231             # Purpose : Alter the running config with new values.
1232             # Returns : A reference to the instalce itself to enable function chaining.
1233             # Arguments : 1. a hashref containing config keys and values.
1234             # Throws : Croaks on invalid invocaiton, invalid args, and, if the resulting
1235             # new config is in some way invalid.
1236             # Notes : Invalid keys in the new keys hashref will be silently ignored.
1237             # See Also :
1238             sub update_config{
1239 0     0 1 0 my @args = @_;
1240 0         0 my $self = shift @args;
1241 0         0 _force_instance($self);
1242            
1243             # validate args
1244 0         0 state $args_check = compile(ConfigOverride);
1245 0         0 my ($new_keys) = $args_check->(@args);
1246            
1247             # clone the current config as a starting point for the new config
1248 0         0 my $new_config = $self->_clone_config();
1249            
1250             # get a reference to the key definitions from the types class
1251 0         0 my $defined_keys = $_TYPES_CLASS->_config_keys();
1252            
1253             # merge the new values into the config
1254 0         0 my $num_keys_updated = 0;
1255             CONFIG_KEY:
1256 0         0 foreach my $key ($_CLASS->defined_config_keys()){
1257             # skip the key if it's not present in the list of new keys
1258 0 0       0 next CONFIG_KEY unless defined $new_keys->{$key};
1259            
1260             # update the key in the new config
1261 0         0 $new_config->{$key} = $new_keys->{$key};
1262 0         0 $num_keys_updated++;
1263 0         0 _debug("updated $key to new value");
1264             }
1265 0         0 _debug("updated $num_keys_updated keys");
1266            
1267             # distil the alphabets in the merged config
1268 0         0 $_CLASS->_distil_alphabets_inplace($new_config);
1269            
1270             # validate the merged config
1271 0 0       0 unless(Config->check($new_config)){
1272 0         0 _error('the updated config is not valid: '.Config->get_message($new_config));
1273             }
1274            
1275             # re-calculate the dictionary cache if needed
1276 0         0 my @cache_all = @{$self->{_CACHE_DICTIONARY_FULL}};
  0         0  
1277 0         0 my @cache_limited = @{$self->{_CACHE_DICTIONARY_LIMITED}};
  0         0  
1278 0 0 0     0 if($new_config->{word_length_min} ne $self->{_CONFIG}->{word_length_min} || $new_config->{word_length_max} ne $self->{_CONFIG}->{word_length_max}){
1279             # re-build the cache of valid words - throws an error if too few words are returned
1280 0         0 @cache_limited = $_CLASS->_filter_word_list(\@cache_all, $new_config->{word_length_min}, $new_config->{word_length_max}, $new_config->{allow_accents});
1281             }
1282            
1283             # if we got here, all is well with the new config, so add it and the caches to the instance
1284 0         0 $self->{_CONFIG} = $new_config;
1285 0         0 $self->{_CACHE_DICTIONARY_LIMITED} = [@cache_limited];
1286            
1287             # update the instance's entropy cache
1288 0         0 $self->_update_entropystats_cache();
1289            
1290             # return a reference to self
1291 0         0 return $self;
1292             }
1293              
1294             #####-SUB-#####################################################################
1295             # Type : INSTANCE
1296             # Purpose : Get the currently loaded RNG object, or, load a new RNG
1297             # Returns : An instance to the loaded RNG object, or, a reference to the
1298             # instance (to enable function chaining) if called as a setter
1299             # Arguments : 1. OPTIONAL - an object that is a Crypt::HSXKPasswd::RNG
1300             # Throws : Croaks on invalid invocation, or invalid args
1301             # Notes :
1302             # See Also :
1303             sub rng{
1304 10     10 1 23 my @args = @_;
1305 10         18 my $self = shift @args;
1306 10         42 _force_instance($self);
1307            
1308             # validate args
1309 10         24 state $args_check = multisig(
1310             [],
1311             [InstanceOf[$_RNG_BASE_CLASS]],
1312             );
1313 10         1564 my ($rng) = $args_check->(@args);
1314            
1315             # if we're a getter, just get and return
1316 10 50       296 unless(defined $rng){
1317 0         0 return $self->{_RNG};
1318             }
1319            
1320             # OK - so we're a getter - carry on!
1321            
1322             # set the RNG
1323 10         25 $self->{_RNG} = $rng;
1324            
1325             # empty the random cache
1326 10         23 $self->{_CACHE_RANDOM} = [];
1327            
1328             # return a reference to self
1329 10         21 return $self;
1330             }
1331              
1332             #####-SUB-######################################################################
1333             # Type : INSTANCE
1334             # Purpose : Return the status of the internal caches within the instnace.
1335             # Returns : A string
1336             # Arguments : NONE
1337             # Throws : Croaks in invalid invocation
1338             # Notes :
1339             # See Also :
1340             sub caches_state{
1341 0     0 0 0 my $self = shift;
1342 0         0 _force_instance($self);
1343              
1344             # generate the string
1345 0         0 my $ans = q{};
1346 0         0 $ans .= 'Loaded Words: '.(scalar @{$self->{_CACHE_DICTIONARY_LIMITED}}).' (out of '.(scalar @{$self->{_CACHE_DICTIONARY_FULL}}).' loaded from the file)'.qq{\n};
  0         0  
  0         0  
1347 0         0 $ans .= 'Cached Random Numbers: '.(scalar @{$self->{_CACHE_RANDOM}}).qq{\n};
  0         0  
1348            
1349             # return it
1350 0         0 return $ans;
1351             }
1352              
1353             #####-SUB-######################################################################
1354             # Type : INSTANCE
1355             # Purpose : Generaete a random password based on the object's loaded config
1356             # Returns : a passowrd as a scalar
1357             # Arguments : NONE
1358             # Throws : Croaks on invalid invocation or on error generating the password
1359             # Notes :
1360             # See Also :
1361             sub password{
1362 10     10 1 21 my $self = shift;
1363 10         35 _force_instance($self);
1364            
1365             #
1366             # Generate the password
1367             #
1368 10         18 my $password = q{};
1369             eval{
1370             #
1371             # start by generating the needed parts of the password
1372             #
1373 10         24 _debug('starting to generate random words');
1374 10         51 my @words = $self->_random_words();
1375 9         50 _debug('got random words='.(join q{, }, @words));
1376 9         42 $self->_transform_case(\@words);
1377 9         33 $self->_substitute_characters(\@words); # TO DO
1378 9         31 my $separator = $self->_separator();
1379 9         35 _debug("got separator=$separator");
1380 9         35 my $pad_char = $self->_padding_char($separator);
1381 9         36 _debug("got pad_char=$pad_char");
1382            
1383             #
1384             # Then assemble the finished password
1385             #
1386            
1387             # start with the words and the separator
1388 9         41 $password = join $separator, @words;
1389 9         36 _debug("assembled base password: $password");
1390            
1391             # next add the numbers front and back
1392 9 100       34 if($self->{_CONFIG}->{padding_digits_before} > 0){
1393 5         19 $password = $self->_random_digits($self->{_CONFIG}->{padding_digits_before}).$separator.$password;
1394             }
1395 9 100       33 if($self->{_CONFIG}->{padding_digits_after} > 0){
1396 5         23 $password = $password.$separator.$self->_random_digits($self->{_CONFIG}->{padding_digits_after});
1397             }
1398 9         44 _debug("added random digits (as configured): $password");
1399            
1400            
1401             # then finally add the padding characters
1402 9 100       44 if($self->{_CONFIG}->{padding_type} eq 'FIXED'){
    50          
1403             # simple fixed padding
1404 5 50       23 if($self->{_CONFIG}->{padding_characters_before} > 0){
1405 5         19 foreach my $c (1..$self->{_CONFIG}->{padding_characters_before}){
1406 10         22 $password = $pad_char.$password;
1407             }
1408             }
1409 5 50       15 if($self->{_CONFIG}->{padding_characters_after} > 0){
1410 5         10 foreach my $c (1..$self->{_CONFIG}->{padding_characters_after}){
1411 10         17 $password .= $pad_char;
1412             }
1413             }
1414             }elsif($self->{_CONFIG}->{padding_type} eq 'ADAPTIVE'){
1415             # adaptive padding
1416 0         0 my $pwlen = length $password;
1417 0 0       0 if($pwlen < $self->{_CONFIG}->{pad_to_length}){
    0          
1418             # if the password is shorter than the target length, padd it out
1419 0         0 while((length $password) < $self->{_CONFIG}->{pad_to_length}){
1420 0         0 $password .= $pad_char;
1421             }
1422             }elsif($pwlen > $self->{_CONFIG}->{pad_to_length}){
1423             # if the password is too long, trim it
1424 0         0 $password = substr $password, 0, $self->{_CONFIG}->{pad_to_length};
1425             }
1426             }
1427 9         46 _debug("added padding (as configured): $password");
1428 9         44 1; # ensure true evaluation on successful execution
1429 10 100       19 }or do{
1430 1         6 _error("Failed to generate password with the following error: $EVAL_ERROR");
1431             };
1432            
1433             # increment the passwords generated counter
1434 9         17 $self->{_PASSWORD_COUNTER}++;
1435            
1436             # return the finished password
1437 9         1718 return $password;
1438             }
1439              
1440             #####-SUB-######################################################################
1441             # Type : INSTANCE
1442             # Purpose : Generate multiple passwords
1443             # Returns : An array of passwords as scalars
1444             # Arguments : 1. the number of passwords to generate as a scalar
1445             # Throws : Croaks on invalid invocation or invalid args
1446             # Notes :
1447             # See Also :
1448             sub passwords{
1449 0     0 1 0 my @args = @_;
1450 0         0 my $self = shift @args;
1451 0         0 _force_instance($self);
1452            
1453             # validate args
1454 0         0 state $args_check = compile(NonZeroPositiveInteger);
1455 0         0 my ($num_pws) = $args_check->(@args);
1456            
1457             # generate the needed passwords
1458 0         0 my @passwords = ();
1459 0         0 my $num_to_do = $num_pws;
1460 0         0 while($num_to_do > 0){
1461 0         0 push @passwords, $self->password(); # could croak
1462 0         0 $num_to_do--;
1463             }
1464            
1465             # return the passwords
1466 0         0 return @passwords;
1467             }
1468              
1469             #####-SUB-######################################################################
1470             # Type : INSTANCE
1471             # Purpose : Generate n passwords and return them, and the entropy stats as a
1472             # JSON string.
1473             # Returns : A JSON string as a scalar representing a hashref contianing an
1474             # array of passwords indexed by 'passwords', and a hashref of
1475             # entropy stats indexed by 'stats'. The stats hashref itself is
1476             # indexed by: 'password_entropy_blind',
1477             # 'password_permutations_blind', 'password_entropy_blind_min',
1478             # 'password_entropy_blind_max', 'password_permutations_blind_max',
1479             # 'password_entropy_seen' & 'password_permutations_seen'
1480             # Arguments : 1. the number of passwords to generate
1481             # Throws : Croaks on invalid invocation, invalid args, if there is a
1482             # problem generating the passwords, statistics, or converting the
1483             # results to a JSON string, or if the JSON module is not
1484             # available.
1485             # Notes :
1486             # See Also :
1487             sub passwords_json{
1488 0     0 1 0 my @args = @_;
1489 0         0 my $self = shift @args;
1490 0         0 _force_instance($self);
1491            
1492             # validate args
1493 0         0 state $args_check = compile(NonZeroPositiveInteger);
1494 0         0 my ($num_pws) = $args_check->(@args);
1495            
1496             # try generate the passwords and stats - could croak
1497 0         0 my @passwords = $self->passwords($num_pws);
1498 0         0 my %stats = $self->stats();
1499            
1500             # generate the hashref containing the results
1501             my $response_obj = {
1502             passwords => [@passwords],
1503             stats => {
1504             password_entropy_blind => $stats{password_entropy_blind},
1505             password_permutations_blind => $_CLASS->_render_bigint($stats{password_permutations_blind}),
1506             password_entropy_blind_min => $stats{password_entropy_blind_min},
1507             password_permutations_blind_min => $_CLASS->_render_bigint($stats{password_permutations_blind_min}),
1508             password_entropy_blind_max => $stats{password_entropy_blind_max},
1509             password_permutations_blind_max => $_CLASS->_render_bigint($stats{password_permutations_blind_max}),
1510             password_entropy_seen => $stats{password_entropy_seen},
1511 0         0 password_permutations_seen => $_CLASS->_render_bigint($stats{password_permutations_seen}),
1512             },
1513             };
1514            
1515             # try generate the JSON string to return
1516 0         0 my $json_string = q{};
1517             eval{
1518 0         0 $json_string = JSON->new()->encode($response_obj);
1519 0         0 1; # ensure truthy evaluation on succesful execution
1520 0 0       0 }or do{
1521 0         0 _error("Failed to render hashref as JSON string with error: $EVAL_ERROR");
1522             };
1523            
1524             # return the JSON string
1525 0         0 return $json_string;
1526             }
1527              
1528             #####-SUB-######################################################################
1529             # Type : INSTANCE
1530             # Purpose : Return statistics about the instance
1531             # Returns : A hash of statistics indexed by the following keys:
1532             # * 'dictionary_source' - the source of the word list
1533             # * 'dictionary_words_total' - the total number of words loaded
1534             # from the dictionary file
1535             # * 'dictionary_words_filtered' - the number of words loaded from
1536             # the dictionary file that meet the lenght criteria set in the
1537             # loaded config
1538             # * 'dictionary_words_percent_available' - the percentage of the
1539             # total dictionary that is avialable for use with the loaded
1540             # config
1541             # * 'dictionary_filter_length_min' - the minimum length world
1542             # permitted by the filter
1543             # * 'dictionary_filter_length_max' - the maximum length world
1544             # permitted by the filter
1545             # * 'dictionary_contains_accents' - whether or not the filtered
1546             # list contains accented letters
1547             # * 'password_entropy_blind_min' - the entropy of the shortest
1548             # password this config can generate from the point of view of a
1549             # brute-force attacker in bits
1550             # * 'password_entropy_blind_max' - the entropy of the longest
1551             # password this config can generate from the point of view of a
1552             # brute-force attacker in bits
1553             # * 'password_entropy_blind' - the entropy of the average length
1554             # of password generated by this configuration from the point of
1555             # view of a brute-force attacker in bits
1556             # * 'password_entropy_seen' - the true entropy of passwords
1557             # generated by this instance assuming the dictionary and config
1558             # are known to the attacker in bits
1559             # * 'password_length_min' - the minimum length of passwords
1560             # generated with this instance's config
1561             # * 'password_length_max' - the maximum length of passwords
1562             # generated with this instance's config
1563             # * 'password_permutations_blind_min' - the number of permutations
1564             # a brute-froce attacker would have to try to be sure of success
1565             # on the shortest possible passwords geneated by this instance
1566             # as a Math::BigInt object
1567             # * 'password_permutations_blind_max' - the number of permutations
1568             # a brute-froce attacker would have to try to be sure of success
1569             # on the longest possible passwords geneated by this instance as
1570             # a Math::BigInt object
1571             # * 'password_permutations_blind' - the number of permutations
1572             # a brute-froce attacker would have to try to be sure of success
1573             # on the average length password geneated by this instance as a
1574             # Math::BigInt object
1575             # * 'password_permutations_seen' - the number of permutations an
1576             # attacker with a copy of the dictionary and config would need to
1577             # try to be sure of cracking a password generated by this
1578             # instance as a Math::BigInt object
1579             # * 'password_random_numbers_required' - the number of random
1580             # numbers needed to generate a single password using the loaded
1581             # config
1582             # * 'passwords_generated' - the number of passwords this instance
1583             # has generated
1584             # * 'randomnumbers_cached' - the number of random numbers
1585             # currently cached within the instance
1586             # * 'randomnumbers_cache_increment' - the number of random numbers
1587             # generated at once to re-plenish the cache when it's empty
1588             # * 'randomnumbers_source' - the name of the class used to
1589             # generate random numbers
1590             # Arguments : NONE
1591             # Throws : Croaks on invalid invocation
1592             # Notes :
1593             # See Also :
1594             sub stats{
1595 10     10 1 20 my $self = shift;
1596 10         20 _force_instance($self);
1597            
1598             # create a hash to assemble all the stats into
1599 10         19 my %stats = ();
1600            
1601             # deal with the config-specific stats
1602 10         36 my %config_stats = $_CLASS->config_stats($self->{_CONFIG});
1603 10         34 $stats{password_length_min} = $config_stats{length_min};
1604 10         18 $stats{password_length_max} = $config_stats{length_max};
1605 10         23 $stats{password_random_numbers_required} = $config_stats{random_numbers_required};
1606            
1607             # deal with the dictionary file
1608 10         44 my %dict_stats = $self->_calcualte_dictionary_stats();
1609 10         33 $stats{dictionary_source} = $dict_stats{source};
1610 10         19 $stats{dictionary_words_total} = $dict_stats{num_words_total};
1611 10         21 $stats{dictionary_words_filtered} = $dict_stats{num_words_filtered};
1612 10         24 $stats{dictionary_words_percent_available} = $dict_stats{percent_words_available};
1613 10         23 $stats{dictionary_filter_length_min} = $dict_stats{filter_length_min};
1614 10         16 $stats{dictionary_filter_length_max} = $dict_stats{filter_length_max};
1615 10         18 $stats{dictionary_contains_accents} = $dict_stats{contains_accents};
1616            
1617             # deal with the entropy stats
1618 10         26 $stats{password_entropy_blind_min} = $self->{_CACHE_ENTROPYSTATS}->{entropy_blind_min};
1619 10         21 $stats{password_entropy_blind_max} = $self->{_CACHE_ENTROPYSTATS}->{entropy_blind_max};
1620 10         25 $stats{password_entropy_blind} = $self->{_CACHE_ENTROPYSTATS}->{entropy_blind};
1621 10         23 $stats{password_entropy_seen} = $self->{_CACHE_ENTROPYSTATS}->{entropy_seen};
1622 10         17 $stats{password_permutations_blind_min} = $self->{_CACHE_ENTROPYSTATS}->{permutations_blind_min};
1623 10         20 $stats{password_permutations_blind_max} = $self->{_CACHE_ENTROPYSTATS}->{permutations_blind_max};
1624 10         23 $stats{password_permutations_blind} = $self->{_CACHE_ENTROPYSTATS}->{permutations_blind};
1625 10         58 $stats{password_permutations_seen} = $self->{_CACHE_ENTROPYSTATS}->{permutations_seen};
1626            
1627             # deal with password counter
1628 10         19 $stats{passwords_generated} = $self->{_PASSWORD_COUNTER};
1629            
1630             # deal with the random number generator
1631 10         12 $stats{randomnumbers_cached} = scalar @{$self->{_CACHE_RANDOM}};
  10         26  
1632 10         46 $stats{randomnumbers_source} = blessed($self->{_RNG});
1633            
1634             # return the stats
1635 10         123 return %stats;
1636             }
1637              
1638             #####-SUB-######################################################################
1639             # Type : INSTANCE
1640             # Purpose : Represent the current state of the instance as a string.
1641             # Returns : Returns a multi-line string as as scalar containing details of the
1642             # loaded dictionary file, config, and caches
1643             # Arguments : NONE
1644             # Throws : Croaks on invalid invocation
1645             # Notes :
1646             # See Also :
1647             sub status{
1648 10     10 1 73 my $self = shift;
1649 10         24 _force_instance($self);
1650            
1651             # assemble the response
1652 10         38 my %stats = $self->stats();
1653 10         29 my $status = q{};
1654            
1655             # the dictionary
1656 10         17 $status .= "*DICTIONARY*\n";
1657 10         45 $status .= "Source: $stats{dictionary_source}\n";
1658 10         26 $status .= "# words: $stats{dictionary_words_total}\n";
1659 10         186 $status .= "# words of valid length: $stats{dictionary_words_filtered} ($stats{dictionary_words_percent_available}%)\n";
1660 10 50       39 $status .= 'Contains Accented Characters: '.($stats{dictionary_contains_accents} ? 'YES' : 'NO')."\n";
1661            
1662             # the config
1663 10         14 $status .= "\n*CONFIG*\n";
1664 10         35 $status .= $self->config_as_string();
1665            
1666             # the random number cache
1667 10         24 $status .= "\n*RANDOM NUMBER CACHE*\n";
1668 10         23 $status .= "Random Number Generator: $stats{randomnumbers_source}\n";
1669 10         28 $status .= "# in cache: $stats{randomnumbers_cached}\n";
1670            
1671             # password statistics
1672 10         21 $status .= "\n*PASSWORD STATISTICS*\n";
1673 10 50       33 if($stats{password_length_min} == $stats{password_length_max}){
1674 0         0 $status .= "Password length: $stats{password_length_max}\n";
1675 0         0 $status .= 'Permutations (brute-force): '.$_CLASS->_render_bigint($stats{password_permutations_blind_max})."\n";
1676             }else{
1677 10         36 $status .= "Password length: between $stats{password_length_min} & $stats{password_length_max}\n";
1678 10         41 $status .= 'Permutations (brute-force): between '.$_CLASS->_render_bigint($stats{password_permutations_blind_min}).q{ & }.$_CLASS->_render_bigint($stats{password_permutations_blind_max}).q{ (average }.$_CLASS->_render_bigint($stats{password_permutations_blind}).")\n";
1679             }
1680 10         40 $status .= 'Permutations (given dictionary & config): '.$_CLASS->_render_bigint($stats{password_permutations_seen})."\n";
1681 10 50       38 if($stats{password_length_min} == $stats{password_length_max}){
1682 0         0 $status .= "Entropy (brute-force): $stats{password_entropy_blind_max}bits\n";
1683             }else{
1684 10         62 $status .= "Entropy (Brute-Force): between $stats{password_entropy_blind_min}bits and $stats{password_entropy_blind_max}bits (average $stats{password_entropy_blind}bits)\n";
1685             }
1686 10         23 $status .= "Entropy (given dictionary & config): $stats{password_entropy_seen}bits\n";
1687 10         26 $status .= "# Random Numbers needed per-password: $stats{password_random_numbers_required}\n";
1688 10         29 $status .= "Passwords Generated: $stats{passwords_generated}\n";
1689            
1690             # debug-only
1691 10 50       30 if($_Types_CLASS::_DEBUG){ ## no critic (ProtectPrivateVars)
1692 0         0 $status .= "\n*DEBUG INFO*\n";
1693 0 0       0 if($_Types_CLASS::_CAN_STACK_TRACE){ ## no critic (ProtectPrivateVars)
1694 0         0 $status .= "Devel::StackTrace IS installed\n";
1695             }else{
1696 0         0 $status .= "Devel::StackTrace is NOT installed\n";
1697             }
1698             }
1699            
1700             # return the status
1701 10         265 return $status;
1702             }
1703              
1704             #
1705             # Regular Subs-----------------------------------------------------------------
1706             #
1707              
1708             #####-SUB-######################################################################
1709             # Type : SUBROUTINE
1710             # Purpose : A functional interface to this library (exported)
1711             # Returns : A random password as a scalar
1712             # Arguments : See the constructor
1713             # Throws : Croaks on error
1714             # Notes : See the Constructor
1715             # See Also : For valid configuarion options see POD documentation below
1716             sub hsxkpasswd{
1717 10     10 1 1100 my @constructor_args = @_;
1718            
1719             # try initialise an xkpasswd object
1720 10         19 my $hsxkpasswd;
1721             eval{
1722 10         52 $hsxkpasswd = $_CLASS->new(@constructor_args);
1723 10         40 1; # ensure truthy evaliation on successful execution
1724 10 50       19 } or do {
1725 0         0 _error("Failed to generate password with the following error: $EVAL_ERROR");
1726             };
1727            
1728             # genereate and return a password - could croak
1729 10         47 return $hsxkpasswd->password();
1730             }
1731              
1732             #
1733             # 'Private' functions ---------------------------------------------------------
1734             #
1735              
1736             #####-SUB-######################################################################
1737             # Type : INSTANCE ('PRIVATE')
1738             # Purpose : Clone the instance's config hashref
1739             # Returns : a hashref
1740             # Arguments : NONE
1741             # Throws : Croaks if called in an invalid way
1742             # Notes :
1743             # See Also :
1744             sub _clone_config{
1745 0     0   0 my $self = shift;
1746 0         0 _force_instance($self);
1747            
1748             # build the clone
1749 0         0 my $clone = $_CLASS->clone_config($self->{_CONFIG});
1750            
1751             # if, and only if, debugging, validate the cloned config so errors in the
1752             # cloning code will trigger an exception
1753 0 0       0 if($self->{debug}){
1754 0 0       0 Config->check($clone) || _error('cloning error - clone is invalid: '.Config->get_message($clone));
1755             }
1756            
1757             # return the clone
1758 0         0 return $clone;
1759             }
1760              
1761             #####-SUB-######################################################################
1762             # Type : CLASS
1763             # Purpose : Distil all alphabets in a config hashref
1764             # Returns : always returns 1 (to keep perlcritic happy)
1765             # Arguments : 1) a config hashref
1766             # Throws : Croaks on invalid invocation or args
1767             # Notes :
1768             # See Also :
1769             sub _distil_alphabets_inplace{
1770 10     10   95 my @args = @_;
1771 10         49 my $class = shift @args;
1772 10         40 _force_class($class);
1773            
1774             # validate args
1775 10         18 state $args_check = compile(Config);
1776 10         852 my ($config) = $args_check->(@args);
1777            
1778             # distil all three possible alphabet keys, if pressent
1779 10 100       131 if($config->{symbol_alphabet}){
1780 6         39 $config->{symbol_alphabet} = [$_CLASS->distil_to_symbol_alphabet($config->{symbol_alphabet})];
1781             }
1782 10 50       47 if($config->{padding_alphabet}){
1783 0         0 $config->{padding_alphabet} = [$_CLASS->distil_to_symbol_alphabet($config->{padding_alphabet})];
1784             }
1785 10 50       28 if($config->{separator_alphabet}){
1786 0         0 $config->{separator_alphabet} = [$_CLASS->distil_to_symbol_alphabet($config->{separator_alphabet})];
1787             }
1788            
1789             # an explicit return
1790 10         22 return 1;
1791             }
1792              
1793             #####-SUB-######################################################################
1794             # Type : CLASS (PRIVATE)
1795             # Purpose : Filter a word list based on word length
1796             # Returns : An array of words as scalars.
1797             # Arguments : 1. a reference to the array of words to filter.
1798             # 2. the minimum allowed word length
1799             # 3. the maximum allowed word length
1800             # 4. OPTIONAL - named argument allow_accents with a value of 0 or
1801             # 1. If 1 is passed, accents will not be stripped from words,
1802             # otherwise they will.
1803             # Throws : Croaks on invalid invocation, or if too few matching words found.
1804             # Notes : Unless the fourth argument is a truthy value, accents will be
1805             # stripped from the words.
1806             # See Also :
1807             sub _filter_word_list{
1808 10     10   101 my @args = @_;
1809 10         74 my $class = shift @args;
1810 10         58 _force_class($class);
1811            
1812             # validate args
1813 10         23 state $args_check = compile(
1814             ArrayRef[Str],
1815             WordLength,
1816             WordLength,
1817             slurpy Dict[allow_accents => Optional[TrueFalse]]
1818             );
1819 10         6696 my ($word_list_ref, $min_len, $max_len, $options) = $args_check->(@args);
1820 10   50     493 my $allow_accents = $options->{allow_accents} || 0;
1821 10 50       32 unless($max_len >= $min_len){
1822 0         0 _error("minimum length (recived $min_len) cannot be greater than maximum length (received $max_len)");
1823             }
1824            
1825             #build the array of words of appropriate length
1826 10         24 my @ans = ();
1827             WORD:
1828 10         14 foreach my $word (@{$word_list_ref}){
  10         21  
1829             # calcualte the grapheme length
1830 11334         25618 my $grapheme_length = $_CLASS->_grapheme_length($word);
1831            
1832             # skip words shorter than the minimum
1833 11334 50       18649 next WORD if $grapheme_length < $min_len;
1834            
1835             # skip words longer than the maximum
1836 11334 100       16701 next WORD if $grapheme_length > $max_len;
1837            
1838             # strip accents unless they are explicitly allowed by the config
1839 10749 50       15468 unless($allow_accents){
1840 10749         21921 $word = unidecode($word);
1841             }
1842            
1843             # store the word in the filtered list
1844 10749         147400 push @ans, $word;
1845             }
1846            
1847             # return the list
1848 10         3007 return @ans;
1849             }
1850              
1851             #####-SUB-######################################################################
1852             # Type : CLASS (PRIVATE)
1853             # Purpose : Determine whether a word list contains accented characters
1854             # Returns : 1 if the word list does contain accented characters, and 0 if it
1855             # does not.
1856             # Arguments : 1. A reference to an array of words to test
1857             # Throws : NOTHING
1858             # Notes :
1859             # See Also :
1860             sub _contains_accented_letters{
1861 0     0   0 my @args = @_;
1862 0         0 my $class = shift @args;
1863 0         0 _force_class($class);
1864            
1865             # validate args
1866 0         0 state $args_check = compile(ArrayRef[Str]);
1867 0         0 my ($word_list_ref) = $args_check->(@args);
1868            
1869             # assume no accented characters, test until 1 is found
1870 0         0 my $accent_found = 0;
1871             WORD:
1872 0         0 foreach my $word (@{$word_list_ref}){
  0         0  
1873             # check for accents by stripping accents and comparing to original
1874 0         0 my $word_accents_stripped = unidecode($word);
1875 0 0       0 unless($word eq $word_accents_stripped){
1876 0         0 $accent_found = 1;
1877 0         0 last WORD;
1878             }
1879             }
1880            
1881             # return the list
1882 0         0 return $accent_found;
1883             }
1884              
1885             #####-SUB-######################################################################
1886             # Type : INSTANCE (PRIVATE)
1887             # Purpose : Generate a random integer greater than 0 and less than a given
1888             # maximum value.
1889             # Returns : A random integer as a scalar.
1890             # Arguments : 1. the min value for the random number (as a positive integer)
1891             # Throws : Croaks if invoked in an invalid way, with invalid args, of if
1892             # there is a problem generating random numbers (should the cache)
1893             # be empty.
1894             # Notes : The random cache is used as the source for the randomness. If the
1895             # random pool is empty, this function will replenish it.
1896             # See Also :
1897             sub _random_int{
1898 91     91   133 my @args = @_;
1899 91         100 my $self = shift @args;
1900 91         220 _force_instance($self);
1901            
1902             # validate args
1903 91         91 state $args_check = compile(NonZeroPositiveInteger);
1904 91         865 my ($max) = $args_check->(@args);
1905            
1906             # calculate the random number
1907 91         703 my $ans = ($self->_rand() * 1_000_000) % $max;
1908            
1909             # return it
1910 90         372 _debug("returning $ans (max=$max)");
1911 90         320 return $ans;
1912             }
1913              
1914             #####-SUB-######################################################################
1915             # Type : INSTANCE (PRIVATE)
1916             # Purpose : Generate a number of random integers.
1917             # Returns : A scalar containing a number of random integers.
1918             # Arguments : 1. The number of random integers to generate
1919             # Throws : Croaks on invalid invocation, or if there is a problem generating
1920             # the needed randomness.
1921             # Notes :
1922             # See Also :
1923             sub _random_digits{
1924 10     10   24 my @args = @_;
1925 10         13 my $self = shift @args;
1926 10         25 _force_instance($self);
1927            
1928             # validate args
1929 10         16 state $args_check = compile(NonZeroPositiveInteger);
1930 10         701 my ($num) = $args_check->(@args);
1931            
1932             # assemble the response
1933 10         74 my $ans = q{};
1934 10         22 foreach my $n (1..$num){
1935 20         35 $ans .= $self->_random_int(10);
1936             }
1937            
1938             # return the response
1939 10         40 return $ans;
1940             }
1941              
1942             #####-SUB-######################################################################
1943             # Type : INSTANCE (PRIVATE)
1944             # Purpose : Return the next random number in the cache, and if needed,
1945             # replenish it.
1946             # Returns : A decimal number between 0 and 1
1947             # Arguments : NONE
1948             # Throws : Croaks if invoked in an invalid way, or if there is problem
1949             # replenishing the random cache.
1950             # Notes :
1951             # See Also :
1952             sub _rand{
1953 91     91   98 my $self = shift;
1954 91         178 _force_instance($self);
1955            
1956             # get the next random number from the cache
1957 91         87 my $num = shift @{$self->{_CACHE_RANDOM}};
  91         223  
1958 91 100       224 if(!defined $num){
1959             # the cache was empty - so try top up the random cache - could croak
1960 15         43 _debug('random cache empty - attempting to replenish');
1961 15         45 $self->_increment_random_cache();
1962            
1963             # try shift again
1964 14         15 $num = shift @{$self->{_CACHE_RANDOM}};
  14         38  
1965             }
1966            
1967             # make sure we got a valid random number
1968 90 50 33     1589 unless(defined $num && $num =~ m/^\d+([.]\d+)?$/sx && $num >= 0 && $num <= 1){
      33        
      33        
1969 0         0 _error('found invalid entry in random cache');
1970             }
1971            
1972             # return the random number
1973 90         321 _debug("returning $num (".(scalar @{$self->{_CACHE_RANDOM}}).' remaining in cache)');
  90         428  
1974 90         357 return $num;
1975             }
1976              
1977             #####-SUB-######################################################################
1978             # Type : INSTANCE (PRIVATE)
1979             # Purpose : Append random numbers to the cache.
1980             # Returns : Always returns 1.
1981             # Arguments : NONE
1982             # Throws : Croaks if incorrectly invoked or if the random generating
1983             # function fails to produce random numbers.
1984             # Notes :
1985             # See Also :
1986             sub _increment_random_cache{
1987 15     15   26 my $self = shift;
1988 15         42 _force_instance($self);
1989            
1990             # genereate the random numbers
1991 15         66 my @random_numbers = $self->{_RNG}->random_numbers($_CLASS->config_random_numbers_required($self->{_CONFIG}));
1992 15         412 _debug('generated '.(scalar @random_numbers).' random numbers ('.(join q{, }, @random_numbers).')');
1993            
1994             # validate them
1995 15 50       48 unless(scalar @random_numbers){
1996 0         0 _error('random function did not return any random numbers');
1997             }
1998 15         34 foreach my $num (@random_numbers){
1999 136 100       712 unless($num =~ m/^1|(0([.]\d+)?)$/sx){
2000 1         10 _error("random function returned and invalid value ($num)");
2001             }
2002             }
2003            
2004             # add them to the cache
2005 14         29 foreach my $num (@random_numbers){
2006 130         87 push @{$self->{_CACHE_RANDOM}}, $num;
  130         219  
2007             }
2008            
2009             # always return 1 (to keep PerlCritic happy)
2010 14         42 return 1;
2011             }
2012              
2013             #####-SUB-######################################################################
2014             # Type : INSTANCE (PRIVATE)
2015             # Purpose : Get the required number of random words from the loaded words
2016             # file
2017             # Returns : An array of words
2018             # Arguments : NONE
2019             # Throws : Croaks on invalid invocation or error generating random numbers
2020             # Notes : The number of words generated is determined by the num_words
2021             # config key.
2022             # See Also :
2023             sub _random_words{
2024 10     10   12 my $self = shift;
2025 10         28 _force_instance($self);
2026            
2027             # get the random words
2028 10         12 my @ans = ();
2029 10         50 _debug('about to generate '.$self->{_CONFIG}->{num_words}.' words');
2030 10         36 while ((scalar @ans) < $self->{_CONFIG}->{num_words}){
2031 36         60 my $word = $self->{_CACHE_DICTIONARY_LIMITED}->[$self->_random_int(scalar @{$self->{_CACHE_DICTIONARY_LIMITED}})];
  36         117  
2032 35         138 _debug("generate word=$word");
2033 35         134 push @ans, $word;
2034             }
2035            
2036             # return the list of random words
2037 9         41 _debug('returning '.(scalar @ans).' words');
2038 9         37 return @ans;
2039             }
2040              
2041             #####-SUB-######################################################################
2042             # Type : INSTANCE (PRIVATE)
2043             # Purpose : Get the separator character to use based on the loaded config.
2044             # Returns : A scalar containing the separator, which could be an empty string.
2045             # Arguments : NONE
2046             # Throws : Croaks on invalid invocation, or if there is a problem generating
2047             # any needed random numbers.
2048             # Notes : The character returned is controlled by the config variable
2049             # separator_character
2050             # See Also :
2051             sub _separator{
2052 9     9   13 my $self = shift;
2053 9         23 _force_instance($self);
2054            
2055             # figure out the separator character
2056 9         18 my $sep = $self->{_CONFIG}->{separator_character};
2057 9 50       33 if ($sep eq 'NONE'){
    100          
2058 0         0 $sep = q{};
2059             }elsif($sep eq 'RANDOM'){
2060 5 50       12 if(defined $self->{_CONFIG}->{separator_alphabet}){
2061 0         0 $sep = $self->{_CONFIG}->{separator_alphabet}->[$self->_random_int(scalar @{$self->{_CONFIG}->{separator_alphabet}})];
  0         0  
2062             }else{
2063 5         10 $sep = $self->{_CONFIG}->{symbol_alphabet}->[$self->_random_int(scalar @{$self->{_CONFIG}->{symbol_alphabet}})];
  5         17  
2064             }
2065             }
2066            
2067             # return the separator character
2068 9         26 return $sep
2069             }
2070              
2071             #####-SUB-######################################################################
2072             # Type : INSTANCE (PRIVATE)
2073             # Purpose : Return the padding character based on the loaded config.
2074             # Returns : A scalar containing the padding character, which could be an
2075             # empty string.
2076             # Arguments : 1. the separator character being used to generate the password
2077             # Throws : Croaks on invalid invocation, or if there is a problem geneating
2078             # any needed random numbers.
2079             # Notes : The character returned is determined by a combination of the
2080             # padding_type & padding_character config variables.
2081             # See Also :
2082             sub _padding_char{
2083 9     9   9 my $self = shift;
2084 9         12 my $sep = shift;
2085            
2086            
2087             # validate args - doing it the old-fassioned way because the separator will
2088             # be an empty string if the separator is set to 'NONE'
2089 9         26 _force_instance($self);
2090 9 50       25 if($sep){
2091 9 50       68 unless(Symbol->check($sep)){
2092 0         0 _error('first argument must be an empty string or a valid Symbol');
2093             }
2094             }
2095            
2096             # if there is no padding character needed, return an empty string
2097 9 100       72 if($self->{_CONFIG}->{padding_type} eq 'NONE'){
2098 4         8 return q{};
2099             }
2100            
2101             # if we got here we do need a character, so generate one as appropriate
2102 5         9 my $padc = $self->{_CONFIG}->{padding_character};
2103 5 50       21 if($padc eq 'SEPARATOR'){
    50          
2104 0         0 $padc = $sep;
2105             }elsif($padc eq 'RANDOM'){
2106 5 50       13 if(defined $self->{_CONFIG}->{padding_alphabet}){
2107 0         0 $padc = $self->{_CONFIG}->{padding_alphabet}->[$self->_random_int(scalar @{$self->{_CONFIG}->{padding_alphabet}})];
  0         0  
2108             }else{
2109 5         11 $padc = $self->{_CONFIG}->{symbol_alphabet}->[$self->_random_int(scalar @{$self->{_CONFIG}->{symbol_alphabet}})];
  5         18  
2110             }
2111             }
2112            
2113             # return the padding character
2114 5         13 return $padc;
2115             }
2116              
2117             #####-SUB-######################################################################
2118             # Type : INSTANCE (PRIVATE)
2119             # Purpose : Apply the case transform (if any) specified in the loaded config.
2120             # Returns : Always returns 1 (to keep PerlCritic happy)
2121             # Arguments : 1. A reference to the array contianing the words to be
2122             # transformed.
2123             # Throws : Croaks on invalid invocation or if there is a problem generating
2124             # any needed random numbers.
2125             # Notes : The transformations applied are controlled by the case_transform
2126             # config variable.
2127             # See Also :
2128             sub _transform_case{
2129 9     9   20 my @args = @_;
2130 9         20 my $self = shift @args;
2131 9         24 _force_instance($self);
2132            
2133             # validate args
2134 9         14 state $args_check = compile(ArrayRef[Str]);
2135 9         834 my ($words_ref) = $args_check->(@args);
2136            
2137             # if the transform is set to nothing, then just return
2138 9 50       122 if($self->{_CONFIG}->{case_transform} eq 'NONE'){
2139 0         0 return 1;
2140             }
2141            
2142             # apply the appropriate transform
2143             ## no critic (ProhibitCascadingIfElse);
2144 9 50       77 if($self->{_CONFIG}->{case_transform} eq 'UPPER'){
    50          
    50          
    50          
    100          
    50          
2145 0         0 foreach my $i (0..((scalar @{$words_ref}) - 1)){
  0         0  
2146 0         0 $words_ref->[$i] = uc $words_ref->[$i];
2147             }
2148             }elsif($self->{_CONFIG}->{case_transform} eq 'LOWER'){
2149 0         0 foreach my $i (0..((scalar @{$words_ref}) - 1)){
  0         0  
2150 0         0 $words_ref->[$i] = lc $words_ref->[$i];
2151             }
2152             }elsif($self->{_CONFIG}->{case_transform} eq 'CAPITALISE'){
2153 0         0 foreach my $i (0..((scalar @{$words_ref}) - 1)){
  0         0  
2154 0         0 $words_ref->[$i] = ucfirst lc $words_ref->[$i];
2155             }
2156             }elsif($self->{_CONFIG}->{case_transform} eq 'INVERT'){
2157 0         0 foreach my $i (0..((scalar @{$words_ref}) - 1)){
  0         0  
2158 0         0 $words_ref->[$i] = lcfirst uc $words_ref->[$i];
2159             }
2160             }elsif($self->{_CONFIG}->{case_transform} eq 'ALTERNATE'){
2161             # randomly decide whether to capitalise on odd or even
2162 5 50       19 my $rand_bias = ($self->_random_int(2) % 2 == 0) ? 1 : 0;
2163 5         10 foreach my $i (0..((scalar @{$words_ref}) - 1)){
  5         23  
2164 15         18 my $word = $words_ref->[$i];
2165 15 100       26 if(($i + $rand_bias) % 2 == 0){
2166 5         9 $word = lc $word;
2167             }else{
2168 10         21 $word = uc $word;
2169             }
2170 15         23 $words_ref->[$i] = $word;
2171             }
2172             }elsif($self->{_CONFIG}->{case_transform} eq 'RANDOM'){
2173 4         6 foreach my $i (0..((scalar @{$words_ref}) - 1)){
  4         21  
2174 20         27 my $word = $words_ref->[$i];
2175 20 100       40 if($self->_random_int(2) % 2 == 0){
2176 8         17 $word = uc $word;
2177             }else{
2178 12         26 $word = lc $word;
2179             }
2180 20         41 $words_ref->[$i] = $word;
2181             }
2182             }
2183             ## use critic
2184            
2185 9         18 return 1; # just to to keep PerlCritic happy
2186             }
2187              
2188             #####-SUB-######################################################################
2189             # Type : INSTANCE (PRIVATE)
2190             # Purpose : Apply any case transforms specified in the loaded config.
2191             # Returns : Always returns 1 (to keep PerlCritic happy)
2192             # Arguments : 1. a reference to an array containing the words that will make up
2193             # the password.
2194             # Throws : Croaks on invalid invocation or invalid args.
2195             # Notes : The substitutions that will be applied are specified in the
2196             # character_substitutions config variable.
2197             # See Also :
2198             sub _substitute_characters{
2199 9     9   18 my @args = @_;
2200 9         15 my $self = shift @args;
2201 9         27 _force_instance($self);
2202            
2203             # validate args
2204 9         12 state $args_check = compile(ArrayRef[Str]);
2205 9         602 my ($words_ref) = $args_check->(@args);
2206            
2207             # if no substitutions are defined, do nothing
2208 9 50 50     137 unless(defined $self->{_CONFIG}->{character_substitutions} && (scalar keys %{$self->{_CONFIG}->{character_substitutions}})){
  0         0  
2209 9         19 return 1;
2210             }
2211            
2212             # If we got here, go ahead and apply the substitutions
2213 0         0 foreach my $i (0..((scalar @{$words_ref}) - 1)){
  0         0  
2214 0         0 my $word = $words_ref->[$i];
2215 0         0 foreach my $char (keys %{$self->{_CONFIG}->{character_substitutions}}){
  0         0  
2216 0         0 my $sub = $self->{_CONFIG}->{character_substitutions}->{$char};
2217 0         0 $word =~ s/$char/$sub/sxg;
2218             }
2219 0         0 $words_ref->[$i] = $word;
2220             }
2221            
2222             # always return 1 to keep PerlCritic happy
2223 0         0 return 1;
2224             }
2225              
2226             #####-SUB-######################################################################
2227             # Type : CLASS (PRIVATE)
2228             # Purpose : Perform sanity checks on all the config key definitions
2229             # Returns : Always returns 1 (to keep PerlCritic happy)
2230             # Arguments : NONE
2231             # Throws : Croaks if there is a problem with a key definition.
2232             # Notes : The function is designed to be called from the constructor when
2233             # in debug mode, so it prints information on what it's doing
2234             # to STDERR.
2235             # See Also :
2236             sub _check_config_key_definitions{
2237             # get a reference to the config key definitions form the Types class
2238 1     1   510 my $key_definitions = $_TYPES_CLASS->_config_keys();
2239            
2240             # loop through each key definition and do some sanity checks
2241 1         2 my $num_problems = 0;
2242 1         2 foreach my $key_name ($_CLASS->defined_config_keys()){
2243 17         465 _debug("checking config key '$key_name'");
2244 17 50       33 unless(ConfigKeyDefinition->check($key_definitions->{$key_name})){
2245 0         0 _warn(ConfigKeyDefinition->get_message($key_definitions->{$key_name}));
2246 0         0 $num_problems++;
2247             }
2248             }
2249 1 50       31 if($num_problems == 0){
2250 1         3 _debug('all config key definitions OK');
2251             }else{
2252 0         0 _error("there are errors in $num_problems config key definitions - fix these before continuing");
2253             }
2254            
2255             # to keep perlcritic happy
2256 1         4 return 1;
2257             }
2258              
2259             #####-SUB-######################################################################
2260             # Type : CLASS (PRIVATE)
2261             # Purpose : Perform sanity checks on all defined presets
2262             # Returns : Always returns 1 (to keep perlcritic happy)
2263             # Arguments : NONE
2264             # Throws : Croaks if there is a problem with a preset.
2265             # Notes : The function is designed to be called from the constructor when
2266             # in debug mode, so it prints information on what it's doing
2267             # to STDERR.
2268             # See Also :
2269             sub _check_preset_definitions{
2270             # get a reference to the preset definitions from the types class
2271 1     1   5 my $preset_defs = $_TYPES_CLASS->_presets();
2272            
2273             # loop through all presets and perform sanity checks
2274 1         2 my $num_problems = 0;
2275 1         2 foreach my $preset_name (sort keys %{$preset_defs}){
  1         9  
2276 8         85 _debug("checking preset '$preset_name'");
2277 8 50       18 unless(PresetDefinition->check($preset_defs->{$preset_name})){
2278 0         0 _warn(PresetDefinition->get_message($preset_defs->{$preset_name}));
2279 0         0 $num_problems++;
2280             }
2281             }
2282 1 50       16 if($num_problems == 0){
2283 1         5 _debug('all presets OK');
2284             }else{
2285 0         0 _error("there are errors in $num_problems presets - fix these before continuing");
2286             }
2287            
2288             # to keep perlcritic happy
2289 1         5 return 1;
2290             }
2291              
2292             #####-SUB-######################################################################
2293             # Type : CLASS (PRIVATE)
2294             # Purpose : Create an RNG object that is as secure as possible.
2295             # Returns : An instance of a class that extends Crypt::HSXKPasswd::RNG.
2296             # Arguments : NONE
2297             # Throws : This function issues a warning if it has to fall back to
2298             # Crypt::HSXKPasswd::RNG::Basic.
2299             # Notes : This function works its way through the constructurs for the
2300             # following RNG classes in the following order, returing the first
2301             # successfully instantiate object:
2302             # 1) Crypt::HSXKPasswd::RNG::Math_Random_Secure (using
2303             # Math::Random::Secure)
2304             # 2) Crypt::HSXKPasswd::RNG::Data_Entropy (using
2305             # Data::Entropy::Algorithms)
2306             # 3) Crypt::HSXKPasswd::RNG::DevUrandom (reads from /dev/urandom)
2307             # 4) Crypt::HSXKPasswd::RNG::Basic (using Perl's built-in rand())
2308             # This ordering is based on security and speed - all but Basic are
2309             # good from a secutrity point of view, but Math::Random::Secure is
2310             # over six times faster than Data::Entropy::Algorithms, so it is
2311             # reduced to second place. Speed tested wth the commands:
2312             # time perl -MMath::Random::Secure -e "foreach my \$n (0..1000000){Math::Random::Secure::rand();}"
2313             # time perl -MData::Entropy::Algorithms -e "foreach my \$n (0..1000000){Data::Entropy::Algorithms::rand();}"
2314             # See Also :
2315             sub _best_available_rng{
2316             # try the good entropy sources in order
2317 9     9   63 my $rng;
2318             eval{
2319 9         77 $rng = Crypt::HSXKPasswd::RNG::Math_Random_Secure->new(); # will return a truthy value on success
2320 9 50       22 }or do{
2321 0         0 _debug("Failed to instantiate a Crypt::HSXKPasswd::RNG::Math_Random_Secure object with error: $EVAL_ERROR");
2322             };
2323 9 50       33 return $rng if $rng;
2324             eval{
2325 0         0 $rng = Crypt::HSXKPasswd::RNG::Data_Entropy->new(); # will return a truthy value on success
2326 0 0       0 }or do{
2327 0         0 _debug("Failed to instantiate a Crypt::HSXKPasswd::RNG::Data_Entropy object with error: $EVAL_ERROR");
2328             };
2329 0 0       0 return $rng if $rng;
2330             eval{
2331 0         0 $rng = Crypt::HSXKPasswd::RNG::DevUrandom->new(); # will return a truthy value on success
2332 0 0       0 }or do{
2333 0         0 _debug("Failed to instantiate a Crypt::HSXKPasswd::RNG::DevUrandom object with error: $EVAL_ERROR");
2334             };
2335 0 0       0 return $rng if $rng;
2336            
2337             # if we got here, no secure RNGs were avaialable, so warn, then return an instance of the basic RNG
2338 0         0 _warn(q{using Perl's built-in rand() function for random number generation. This is secure enough for most users, but you can get more secure random numbers by installing Math::Random::Secure or Data::Entropy::Algorithms});
2339 0         0 return Crypt::HSXKPasswd::RNG::Basic->new();
2340             }
2341              
2342             #####-SUB-######################################################################
2343             # Type : INSTANCE (PRIVATE)
2344             # Purpose : Gather entropy stats for the combination of the loaded config
2345             # and dictionary.
2346             # Returns : A hash of stats indexed by:
2347             # * 'permutations_blind_min' - the number of permutations to be
2348             # tested by an attacker with no knowledge of the dictionary file
2349             # used, or the config used, assuming the minimum possible
2350             # password length from the given config (as BigInt)
2351             # * 'permutations_blind_max' - the number of permutations to be
2352             # tested by an attacker with no knowledge of the dictionary file
2353             # used, or the cofig file used, assuming the maximum possible
2354             # password length fom the given config (as BigInt)
2355             # * 'permutations_blind' - the number of permutations for the
2356             # average password length for the given config (as BigInt)
2357             # * 'permutations_seen' - the number of permutations to be tested
2358             # by an attacker with full knowledge of the dictionary file and
2359             # configuration used (as BigInt)
2360             # * 'entropy_blind_min' - permutations_blind_min converted to bits
2361             # * 'entropy_blind_max' - permutations_blind_max converted to bits
2362             # * 'entropy_blind' - permutations_blind converted to bits
2363             # * 'entropy_seen' - permutations_seen converted to bits
2364             # Arguments : NONE
2365             # Throws : Croaks on invalid invocation
2366             # Notes : This function uses config_stats() to determined the longest and
2367             # shortest password lengths, so the caveat that function has
2368             # when it comes to multi-character substitutions applies here too.
2369             # This function assumes no accented characters (at least for now).
2370             # For the blind calculations, if any single symbol is present, a
2371             # search-space of 33 symbols is assumed (same as password
2372             # haystacks page)
2373             # See Also : config_stats()
2374             sub _calculate_entropy_stats{
2375 10     10   20 my $self = shift;
2376 10         35 _force_instance($self);
2377            
2378 10         26 my %ans = ();
2379            
2380             # get the password length details for the config
2381 10         35 my %config_stats = $_CLASS->config_stats($self->{_CONFIG}, suppress_warnings => 1);
2382 10         111 my $b_length_min = Math::BigInt->new($config_stats{length_min});
2383 10         565 my $b_length_max = Math::BigInt->new($config_stats{length_max});
2384            
2385             # calculate the blind permutations - (based purely on length and alphabet)
2386 10         203 my $alphabet_count = 26; # all passwords have at least one case of letters
2387 10 50       66 if($self->{_CONFIG}->{case_transform} =~ m/^(ALTERNATE)|(CAPITALISE)|(INVERT)|(RANDOM)$/sx){
2388 10         19 $alphabet_count += 26; # these configs guarantee a mix of cases
2389             }
2390 10 100 66     52 if($self->{_CONFIG}->{padding_digits_before} > 0 || $self->{_CONFIG}->{padding_digits_after} > 0){
2391 6         171 $alphabet_count += 10; # these configs guarantee digits in the mix
2392             }
2393 10 50 33     39 if($self->_passwords_will_contain_symbol() || $self->{_CACHE_CONTAINS_ACCENTS}){
2394 10         18 $alphabet_count += 33; # the config almost certainly includes a symbol, so add 33 to the alphabet (like password haystacks does)
2395             }
2396 10         32 my $b_alphabet_count = Math::BigInt->new($alphabet_count);
2397 10         292 my $length_avg = round(($config_stats{length_min} + $config_stats{length_max})/2);
2398 10         190 $ans{permutations_blind_min} = $b_alphabet_count->copy()->bpow($b_length_min); #$alphabet_count ** $length_min;
2399 10         2343 _debug('got permutations_blind_min='.$ans{permutations_blind_min});
2400 10         37 $ans{permutations_blind_max} = $b_alphabet_count->copy()->bpow($b_length_max); #$alphabet_count ** $length_max;
2401 10         2330 _debug('got permutations_blind_max='.$ans{permutations_blind_max});
2402 10         35 $ans{permutations_blind} = $b_alphabet_count->copy()->bpow(Math::BigInt->new($length_avg)); #$alphabet_count ** $length_avg;
2403 10         2434 _debug('got permutations_blind='.$ans{permutations_blind});
2404            
2405             # calculate the seen permutations
2406 10         15 my $num_words = scalar @{$self->{_CACHE_DICTIONARY_LIMITED}};
  10         28  
2407 10         37 my $b_num_words = Math::BigInt->new($num_words);
2408 10         294 my $b_seen_perms = Math::BigInt->new('0');
2409             # start with the permutations from the chosen words
2410 10         781 $b_seen_perms->badd($b_num_words->copy()->bpow(Math::BigInt->new($self->{_CONFIG}->{num_words}))); # += $num_words ** $self->{_CONFIG}->{num_words};
2411             # then add the extra randomness from the case transformations (if any)
2412 10 100       1594 if($self->{_CONFIG}->{case_transform} eq 'RANDOM'){
    50          
2413             # multiply by two for each word
2414 4         20 for my $n (1..$self->{_CONFIG}->{num_words}){
2415 20         1073 $b_seen_perms->bmul(Math::BigInt->new(2));
2416             }
2417             }elsif($self->{_CONFIG}->{case_transform} eq 'ALTERNATE'){
2418             # multiply by two for the one random decision about whether or capitalise the odd or even words
2419 6         17 $b_seen_perms->bmul(Math::BigInt->new(2));
2420             }
2421             # multiply in the permutations from the separator (if any - i.e. if it's randomly chosen)
2422 10 100       766 if($self->{_CONFIG}->{separator_character} eq 'RANDOM'){
2423 6 50       23 if(defined $self->{_CONFIG}->{separator_alphabet}){
2424 0         0 $b_seen_perms->bmul(Math::BigInt->new(scalar @{$self->{_CONFIG}->{separator_alphabet}}));
  0         0  
2425             }else{
2426 6         9 $b_seen_perms->bmul(Math::BigInt->new(scalar @{$self->{_CONFIG}->{symbol_alphabet}}));
  6         27  
2427             }
2428             }
2429             # multiply in the permutations from the padding character (if any - i.e. if it's randomly chosen)
2430 10 100 66     486 if($self->{_CONFIG}->{padding_type} ne 'NONE' && $self->{_CONFIG}->{padding_character} eq 'RANDOM'){
2431 6 50       19 if(defined $self->{_CONFIG}->{padding_alphabet}){
2432 0         0 $b_seen_perms->bmul(Math::BigInt->new(scalar @{$self->{_CONFIG}->{padding_alphabet}}));
  0         0  
2433             }else{
2434 6         12 $b_seen_perms->bmul(Math::BigInt->new(scalar @{$self->{_CONFIG}->{symbol_alphabet}}));
  6         34  
2435             }
2436             }
2437             # multiply in the permutations from the padding digits (if any)
2438 10         413 my $num_padding_digits = $self->{_CONFIG}->{padding_digits_before} + $self->{_CONFIG}->{padding_digits_after};
2439 10         69 while($num_padding_digits > 0){
2440 24         43 $b_seen_perms->bmul(Math::BigInt->new('10'));
2441 24         1437 $num_padding_digits--;
2442             }
2443 10         23 $ans{permutations_seen} = $b_seen_perms;
2444 10         76 _debug('got permutations_seen='.$ans{permutations_seen});
2445            
2446             # calculate the entropy values based on the permutations
2447 10         74 $ans{entropy_blind_min} = $ans{permutations_blind_min}->copy()->blog(2)->numify();
2448 10         4391 _debug('got entropy_blind_min='.$ans{entropy_blind_min});
2449 10         43 $ans{entropy_blind_max} = $ans{permutations_blind_max}->copy()->blog(2)->numify();
2450 10         4482 _debug('got entropy_blind_max='.$ans{entropy_blind_max});
2451 10         38 $ans{entropy_blind} = $ans{permutations_blind}->copy()->blog(2)->numify();
2452 10         4123 _debug('got entropy_blind='.$ans{entropy_blind});
2453 10         38 $ans{entropy_seen} = $ans{permutations_seen}->copy()->blog(2)->numify();
2454 10         2545 _debug('got entropy_seen='.$ans{entropy_seen});
2455            
2456             # return the stats
2457 10         139 return %ans;
2458             }
2459              
2460             #####-SUB-######################################################################
2461             # Type : INSTANCE (PRIVATE)
2462             # Purpose : Calculate statistics on the loaded dictionary file
2463             # Returns : A hash of statistics indexed by:
2464             # * 'source' - the source for the word list
2465             # * 'filter_length_min' - the minimum allowed word length
2466             # * 'filter_length_max' - the maximum allowed word length
2467             # * 'num_words_total' - the number of words in the un-filtered
2468             # dictionary file
2469             # * 'num_words_filtered' - the number of words after filtering on
2470             # size limitations
2471             # * 'percent_words_available' - the percentage of the un-filtered
2472             # words remaining in the filtered words list
2473             # * 'contains_accents' - whether or not the filtered word list
2474             # contains accented letter
2475             # Arguments : NONE
2476             # Throws : Croaks on invalid invocation
2477             # Notes :
2478             # See Also :
2479             sub _calcualte_dictionary_stats{
2480 10     10   16 my $self = shift;
2481 10         45 _force_instance($self);
2482            
2483             # create a hash to aggregate the stats into
2484 10         23 my %ans = ();
2485            
2486             # deal with agregate numbers first
2487 10         66 $ans{source} = $self->{_DICTIONARY_SOURCE}->source();
2488 10         14 $ans{num_words_total} = scalar @{$self->{_CACHE_DICTIONARY_FULL}};
  10         32  
2489 10         12 $ans{num_words_filtered} = scalar @{$self->{_CACHE_DICTIONARY_LIMITED}};
  10         28  
2490 10         80 $ans{percent_words_available} = round(($ans{num_words_filtered}/$ans{num_words_total}) * 100);
2491 10         183 $ans{filter_length_min} = $self->{_CONFIG}->{word_length_min};
2492 10         20 $ans{filter_length_max} = $self->{_CONFIG}->{word_length_max};
2493 10         22 $ans{contains_accents} = $self->{_CACHE_CONTAINS_ACCENTS};
2494            
2495             # return the stats
2496 10         64 return %ans;
2497             }
2498              
2499             #####-SUB-######################################################################
2500             # Type : INSTANCE (PRIVATE)
2501             # Purpose : A function to check if passwords genereated with the loaded
2502             # config would contian a symbol
2503             # Returns : 1 if the config will produce passwords with a symbol, or 0
2504             # otherwise
2505             # Arguments : NONE
2506             # Throws : Croaks on invalid invocation
2507             # Notes : This function is used by _calculate_entropy_stats() to figure out
2508             # whether or not there are symbols in the alphabet when calculating
2509             # the brute-force entropy.
2510             # See Also : _calculate_entropy_stats()
2511             sub _passwords_will_contain_symbol{
2512 10     10   10 my $self = shift;
2513 10         38 _force_instance($self);
2514            
2515             # assume no symbol, if we find one, set to 1
2516 10         13 my $symbol_used = 0;
2517            
2518             ## no critic (ProhibitEnumeratedClasses);
2519             # first check the padding
2520 10 100       35 if($self->{_CONFIG}->{padding_type} ne 'NONE'){
2521 6 50       17 if($self->{_CONFIG}->{padding_character} eq 'RANDOM'){
2522 6 50       17 if(defined $self->{_CONFIG}->{padding_alphabet}){
2523 0         0 my $all_pad_chars = join q{}, @{$self->{_CONFIG}->{padding_alphabet}};
  0         0  
2524 0 0       0 if($all_pad_chars =~ m/[^0-9a-zA-Z]/sx){ # if we have just one non-word character
2525 0         0 $symbol_used = 1;
2526             }
2527             }else{
2528 6         11 my $all_pad_chars = join q{}, @{$self->{_CONFIG}->{symbol_alphabet}};
  6         22  
2529 6 50       27 if($all_pad_chars =~ m/[^0-9a-zA-Z]/sx){ # if we have just one non-word character
2530 6         15 $symbol_used = 1;
2531             }
2532             }
2533             }else{
2534 0 0       0 if($self->{_CONFIG}->{padding_character} =~ m/[^0-9a-zA-Z]/sx){ # the padding character is not a word character
2535 0         0 $symbol_used = 1;
2536             }
2537             }
2538             }
2539            
2540             # then check the separator
2541 10 50       32 if($self->{_CONFIG}->{separator_character} ne 'NONE'){
2542 10 100       31 if($self->{_CONFIG}->{separator_character} eq 'RANDOM'){
2543 6 50       20 if(defined $self->{_CONFIG}->{separator_alphabet}){
2544 0         0 my $all_sep_chars = join q{}, @{$self->{_CONFIG}->{separator_alphabet}};
  0         0  
2545 0 0       0 if($all_sep_chars =~ m/[^0-9a-zA-Z]/sx){ # if we have just one non-word character
2546 0         0 $symbol_used = 1;
2547             }
2548             }else{
2549 6         8 my $all_sep_chars = join q{}, @{$self->{_CONFIG}->{symbol_alphabet}};
  6         18  
2550 6 50       23 if($all_sep_chars =~ m/[^0-9a-zA-Z]/sx){ # if we have just one non-word character
2551 6         11 $symbol_used = 1;
2552             }
2553             }
2554             }else{
2555 4 50       18 if($self->{_CONFIG}->{separator_character} =~ m/[^0-9a-zA-Z]/sx){ # the separator is not a word character
2556 4         8 $symbol_used = 1;
2557             }
2558             }
2559             }
2560             ## use critic
2561            
2562             # return
2563 10         32 return $symbol_used;
2564             }
2565              
2566             #####-SUB-######################################################################
2567             # Type : INSTANCE (PRIVATE)
2568             # Purpose : Update the entropy stats cache (and warn of low entropy if
2569             # appropriate)
2570             # Returns : always returns 1 (to keep perlcritic happy)
2571             # Arguments : NONE
2572             # Throws : Croaks on invalid invocation
2573             # Notes : This function should only be called from config() or dictionary().
2574             # The entropy is calculated with _calculate_entropy_stats(), and a
2575             # reference to the hash returned from that function is stored in
2576             # $self->{_CACHE_ENTROPYSTATS}.
2577             # See Also : _calculate_entropy_stats(), config() & dictionary()
2578             sub _update_entropystats_cache{
2579 20     20   39 my $self = shift;
2580 20         75 _force_instance($self);
2581            
2582             # do nothing if the dictionary has not been loaded yet (should only happen while the constructor is building an instance)
2583 20 100 66     236 return 1 unless($self->{_DICTIONARY_SOURCE} && blessed($self->{_DICTIONARY_SOURCE}) && $self->{_DICTIONARY_SOURCE}->isa($_DICTIONARY_BASE_CLASS));
      66        
2584            
2585             # calculate and store the entropy stats
2586 10         108 my %stats = $self->_calculate_entropy_stats();
2587 10         34 $self->{_CACHE_ENTROPYSTATS} = \%stats;
2588            
2589             # warn if we need to
2590 10 50       34 unless(uc $_ENTROPY_WARNINGS eq 'NONE'){
2591             # blind warnings are always needed if the level is not 'NONE'
2592 0 0       0 if($self->{_CACHE_ENTROPYSTATS}->{entropy_blind_min} < $_ENTROPY_MIN_BLIND){
2593 0         0 _warn('for brute force attacks, the combination of the loaded config and dictionary produces an entropy of '.$self->{_CACHE_ENTROPYSTATS}->{entropy_blind_min}.'bits, below the minimum recommended '.$_ENTROPY_MIN_BLIND.'bits');
2594             }
2595            
2596             # seen warnings if the cut-off is not 'BLIND'
2597 0 0       0 unless(uc $_ENTROPY_WARNINGS eq 'BLIND'){
2598 0 0       0 if($self->{_CACHE_ENTROPYSTATS}->{entropy_seen} < $_ENTROPY_MIN_SEEN){
2599 0         0 _warn('for attacks assuming full knowledge, the combination of the loaded config and dictionary produces an entropy of '.$self->{_CACHE_ENTROPYSTATS}->{entropy_seen}.'bits, below the minimum recommended '.$_ENTROPY_MIN_SEEN.'bits');
2600             }
2601             }
2602             }
2603            
2604             # to keep perl critic happy
2605 10         24 return 1;
2606             }
2607              
2608             #####-SUB-######################################################################
2609             # Type : CLASS (PRIVATE)
2610             # Purpose : To nicely print a Math::BigInt object
2611             # Returns : a string representing the object's value in scientific notation
2612             # with 1 digit before the decimal and 2 after
2613             # Arguments : 1. a Math::BigInt object
2614             # Throws : Croaks on invalid invocation or args
2615             # Notes :
2616             # See Also :
2617             sub _render_bigint{
2618 40     40   217 my @args = @_;
2619 40         134 my $class = shift @args;
2620 40         100 _force_class($class);
2621            
2622             # validate args
2623 40         52 state $args_check = compile(InstanceOf['Math::BigInt']);
2624 40         1353 my ($bigint) = $args_check->(@args);
2625            
2626             # convert the bigint to an array of characters
2627 40         428 my @chars = split //sx, "$bigint";
2628            
2629             # render nicely
2630 40 50       1578 if(scalar @chars < 3){
2631 0         0 return q{}.join q{}, @chars;
2632             }
2633             # start with the three most signifficant digits (as a decimal)
2634 40         73 my $ans = q{}.$chars[0].q{.}.$chars[1].$chars[2];
2635             # then add the scientific notation bit
2636 40         67 $ans .= 'x10^'.(scalar @chars - 1);
2637            
2638             # return the result
2639 40         235 return $ans;
2640             }
2641              
2642             #####-SUB-######################################################################
2643             # Type : CLASS (PRIVATE)
2644             # Purpose : Get the so-called 'grapheme length' of a unicode string, that is
2645             # to say, the length of a word where a letter with an accent counts
2646             # as a single letter.
2647             # Returns : An integer
2648             # Arguments : 1) the string to get the length of
2649             # Throws : Croaks on invalid invocation and invalid args
2650             # Notes : Perl, by default, will consider accented letters as having a
2651             # length of two. This function uses a very common algorythm
2652             # recommended all over the internet, including in the Perl Unicode
2653             # cookbook: http://search.cpan.org/~shay/perl-5.20.2/pod/perlunicook.pod
2654             # Before resorting to this technique, I tried to use the
2655             # grapheme_length function from Unicode::Util, but it proved
2656             # unacceptably slow.
2657             # See Also :
2658             sub _grapheme_length{
2659 11334     11334   48481 my @args = @_;
2660 11334         35595 my $class = shift @args;
2661 11334         22326 _force_class($class);
2662            
2663             # validate args
2664 11334         10101 state $args_check = compile(Str);
2665 11334         21125 my ($string) = $args_check->(@args);
2666            
2667             # do the calculation
2668 11334         69714 my $grapheme_length = 0;
2669 11334         231822 while($string =~ /\X/gsx){$grapheme_length++};
  63391         116004  
2670            
2671             # return the result
2672 11334         19894 return $grapheme_length;
2673             }
2674              
2675             1; # because Perl is just a little bit odd :)
2676             __END__