Crypt-HSXKPasswd
view release on metacpan or search on metacpan
bin/hsxkpasswd view on Meta::CPAN
# === Process Args ============================================================#
#
# parse the commandline options
my $verbose = 0;
my %cmd_args = ();
GetOptions(
#
# -- the 'do and exit' options --
#
'help|h' => sub{
pod2usage(
-exitval => 0,
-verbose => 2,
);
},
'version|v' => sub{
pod2usage(
-exitval => 0,
-verbose => 99,
-sections => 'NAME',
);
},
'test-config|t=s' => sub{
my ($opt_name, $val) = @_;
unless(-f $val){
byeee("file $val does not exist");
}
open my $CONFIG_FH, '<', $val or byee("failed to read file $val with error:\n* $OS_ERROR");
my $json_string = do{local $/ = undef; <$CONFIG_FH>};
close $CONFIG_FH;
my $config;
eval{
$config = decode_json($json_string);
1; # explicit evaluation
}or do{
byeee("failed to parse the config JSON string:\n* source file: $val\n* parse error: $EVAL_ERROR");
};
say 'Distilling the config down to just valid keys ...';
$config = Crypt::HSXKPasswd->distil_to_config_keys($config, warn_invalid_key_names => 1);
say q{}; # empty line
say 'Validing the distilled config ...';
my $is_valid = Config->check(Config->coerce($config));
if($is_valid){
say '** Config OK **';
}else{
say '** Config INVALID **';
say Config->get_message($config);
exit 1;
}
exit 0;
},
'test-rcfile=s' => sub{
my ($opt_name, $val) = @_;
unless(-f $val){
byeee("file $val does not exist");
}
open my $RCFILE_FH, '<', $val or byee("failed to read file $val with error:\n* $OS_ERROR");
my $json_string = do{local $/ = undef; <$RCFILE_FH>};
close $RCFILE_FH;
my $rcdata;
eval{
$rcdata = decode_json($json_string);
1; # explicit evaluation
}or do{
byeee("failed to parse the hsxkpasswdrc file's content as JSON:\n* source file: $val\n* parse error: $EVAL_ERROR");
};
say 'Validing the converted datatsructure from the hsxkpasswdrc file ...';
my $is_valid = RCFileData->check($rcdata);
if($is_valid){
say '** hsxkpasswdrc data OK **';
}else{
say '** hsxkpasswdrc data INVALID **';
say RCFileData->get_message($rcdata);
exit 1;
}
exit 0;
},
#
# -- the delayed 'do and exit' options --
#
'list-presets|l' => sub{
my ($opt_name, $val) = @_;
$cmd_args{do_list_presets} = 1;
},
#
# -- the regular data options --
#
'preset|p=s' => sub{
my ($opt_name, $val) = @_;
my $preset_name = uc $val; # convert to upper-case
unless(UppercaseIdentifier->check($preset_name)){
byeee("$val is not a valid preset name, should only contain un-accented letters, digits and underscores");
}
$cmd_args{preset} = $preset_name;
},
'overrides|o=s' => sub{
my ($opt_name, $val) = @_;
my $overrides = {};
eval{
$overrides = decode_json($val);
1; # explicit evaluation
}or do{
byeee("failed to parse JSON preset overrides string with error:\n* $EVAL_ERROR");
};
unless(ConfigOverride->check(ConfigOverride->coerce($overrides))){
byeee("invalid preset overrides:\n* ".ConfigOverride->get_message($overrides));
}
$cmd_args{preset_overrides} = $overrides;
},
'config-file|c=s' => sub{
my ($opt_name, $val) = @_;
unless(-f $val){
byeee("file $val does not exist");
}
open my $CONFIG_FH, '<', $val or byee("failed to read file $val with error:\n* $OS_ERROR");
my $json_string = do{local $/ = undef; <$CONFIG_FH>};
close $CONFIG_FH;
my $config = {};
eval{
$config = decode_json($json_string);
1; # explicit evaluation
}or do{
byeee("failed to parse JSON config string from file $val with error:\n* $EVAL_ERROR");
};
unless(Config->check(Config->coerce($config))){
byeee("invalid config from file $val:\n* ".Config->get_message($config));
}
$cmd_args{config} = $config;
},
'dict-file|d=s' => sub{
my ($opt_name, $val) = @_;
unless(NonEmptyString->check($val)){
byee('--dict-file cannot specify an empty string');
}
unless(-f $val){
byeee("file $val does not exist");
}
$cmd_args{'dict-file'} = $val;
},
'dict-pkg=s' => sub{
my ($opt_name, $val) = @_;
unless(PerlPackageName->check($val)){
byeee("$val is not a valid Perl Package name");
}
$cmd_args{'dict-pkg'} = $val;
},
'dict-pkg-args=s' => sub{
my ($opt_name, $val) = @_;
my $args = [];
eval{
$args = decode_json($val);
1; # explicit evaluation
}or do{
byeee("failed to parse --dict-pkg-args as JSON with error:\n* $EVAL_ERROR");
};
unless(ArrayRef->check($args)){
byeee('--dict-pkg-args did not specify JSON representing an array');
}
$cmd_args{'dict-pkg-args'} = $args;
},
'rng-pkg|r=s' => sub{
my ($opt_name, $val) = @_;
unless(PerlPackageName->check($val)){
byeee("$val is not a valid Perl Package name");
}
$cmd_args{'rng-pkg'} = $val;
},
'rng-pkg-args=s' => sub{
my ($opt_name, $val) = @_;
my $args = [];
eval{
$args = decode_json($val);
1; # explicit evaluation
}or do{
byeee("failed to parse --rng-pkg-args as JSON with error:\n* $EVAL_ERROR");
};
unless(ArrayRef->check($args)){
byeee('--rng-pkg-args did not specify JSON representing an array');
}
$cmd_args{'rng-pkg-args'} = $args;
},
'rcfile=s' => sub{
my ($opt_name, $val) = @_;
if($val eq 'NONE'){
$cmd_args{no_rcdata} = 1;
}else{
$cmd_args{rcdata} = load_rcfile($val);
}
},
'verbose' => \$verbose,
'warn|w=s' => sub{
my ($opt_name, $val) = @_;
unless(EntropyWarningLevel->check($val)){
byeee('invalid entropy warning level: '.EntropyWarningLevel->get_message($val));
}
$cmd_args{warn} = uc $val; # make sure it's upper case
},
) or pod2usage(
-exitval => 1,
-verbose => 0,
);
# process the bare args
my $num_pwds = shift @ARGV;
if($num_pwds){
# validate the passed number of args
unless(NonZeroPositiveInteger->check($num_pwds)){
say 'ERROR - invalid number of passwords requested:';
say '* '.NonZeroPositiveInteger->get_message($num_pwds);
exit 1;
}
}else{
$num_pwds = 1;
}
#
# === Try load an hsxkpasswdrc file if possible ================================#
#
# try determine the user's home dir and .hsxkpasswdrc file path
my @home_dir = File::Spec->splitdir(File::HomeDir->my_home());
my $dot_hsxkpasswdrc_path = File::Spec->catfile(@home_dir, '.hsxkpasswdrc');
# start with blank rcdata
my $rcdata = {};
# unless we're skipping rcdata completely, try from the args, then from ~/hsxkpasswdrc
if($cmd_args{no_rcdata}){
note('loading of hsxkpasswdrc files disabled with --rcfile=NONE');
}else{
if($cmd_args{rcdata}){
$rcdata = $cmd_args{rcdata};
note('using hsxkpasswdrc file from --rcfile option');
}elsif(-f $dot_hsxkpasswdrc_path){
$rcdata = load_rcfile($dot_hsxkpasswdrc_path);
note("using hsxkpasswdrc file $dot_hsxkpasswdrc_path");
}else{
note('no hsxkpasswdrc file loaded');
}
}
#
# === List presets and exit if --list-presets =================================#
#
if($cmd_args{do_list_presets}){
# merge the presets into a single array
my %preset_lookup = ();
my %standard_preset_lookup = ();
my %custom_preset_lookup = ();
foreach my $preset (Crypt::HSXKPasswd->defined_presets()){
$preset_lookup{$preset} = 1;
$standard_preset_lookup{$preset} = 1;
}
if($rcdata->{custom_presets}){
foreach my $preset (keys %{$rcdata->{custom_presets}}){
$preset_lookup{$preset} = 1;
$custom_preset_lookup{$preset} = 1;
}
}
# if there are custom presets, print out the detail if in verbose mode
if($verbose && $rcdata->{custom_presets}){
note('custom presets (from hsxkpasswdrc file): '.(join q{, }, keys %custom_preset_lookup));
my @unoverriden_standard_presets = ();
my @overridden_standard_presets = ();
foreach my $preset (sort keys %standard_preset_lookup){
if($custom_preset_lookup{$preset}){
push @overridden_standard_presets, $preset;
}else{
push @unoverriden_standard_presets, $preset;
}
}
if(scalar @unoverriden_standard_presets){
note('available standard presets: '.(join q{, }, @unoverriden_standard_presets));
}
if(scalar @overridden_standard_presets){
note('UN-AVAILABLE standard presets (custom presets with same name): '.(join q{, }, @overridden_standard_presets));
}
say {*STDERR} q{}; # print a blank line to STDERR to make the output easier to read
}
# always print a de-duplicated list of all defined presets
say join q{, }, sort keys %preset_lookup;
# exit with success
exit 0;
}
#
# === Set the Warning Level (if specified) ====================================#
#
if($cmd_args{warn}){
Crypt::HSXKPasswd->module_config('ENTROPY_WARNINGS', $cmd_args{warn});
note('using entropy warning level specified with --warn option');
}elsif($rcdata->{default_entropy_warnings}){
Crypt::HSXKPasswd->module_config('ENTROPY_WARNINGS', $rcdata->{default_entropy_warnings});
note('using entropy warning level specified in hsxkpasswdrc file');
}else{
note('no custom entropy warning level set');
}
#
# === Assemble the options for the HSXKPasswd constructor =====================#
#
my %hsxkpwd_args = ();
# deal with the config - first try a directly specified config, then a preset,
# then fall back to the default config
if($cmd_args{config}){
$hsxkpwd_args{config} = $cmd_args{config};
note('using config from file specified with --config-file option');
}elsif($cmd_args{preset}){
# if there is a matching custom preset, use it to generate a config,
# otherwise treat the config as a standard config
if($rcdata->{custom_presets} && $rcdata->{custom_presets}->{$cmd_args{preset}}){
# we are a custom preset
my $config = $rcdata->{custom_presets}->{$cmd_args{preset}}->{config};
# apply any overrides if needed
if($cmd_args{preset_overrides}){
# save the keys into the config
foreach my $key (keys %{$cmd_args{preset_overrides}}){
$config->{$key} = $cmd_args{preset_overrides}->{$key};
}
# validate the resulting config
unless(Config->check($config)){
byeee('the custom preset combined with the specified overrides produces an invalid config: '.Config->get_message($config));
}
}
# save to the hsxkpasswd arguments object
$hsxkpwd_args{config} = $config;
note(qq{using config from custom preset '$cmd_args{preset}'});
}else{
# treat as a standard config - make sure it exists, then use
unless(PresetName->check($cmd_args{preset})){
byeee("no preset exists with the name '$cmd_args{preset}' (you can see all defined presets with: hsxkpasswd --list-presets)");
}
$hsxkpwd_args{preset} = $cmd_args{preset};
if($cmd_args{preset_overrides}){
$hsxkpwd_args{preset_overrides} = $cmd_args{preset_overrides};
}
note(qq{using standard preset '$cmd_args{preset}'});
}
}else{
# if the preset DEFUALT is ovrridden by the rcdata, us it
if($rcdata->{custom_presets} && $rcdata->{custom_presets}->{'DEFAULT'}){
$hsxkpwd_args{config} = $rcdata->{custom_presets}->{'DEFAULT'}->{config};
note('using custom default config from hsxkpasswdrc file');
}else{
note('using standard default config');
}
}
# deal with the dictionary - the commandline takes precedence, then the rc file
if($cmd_args{'dict-file'}){
$hsxkpwd_args{dictionary_file} = $cmd_args{'dict-file'};
note('using dictionary file from --dict-file option');
}elsif($cmd_args{'dict-pkg'}){
$hsxkpwd_args{dictionary} = load_dictionary(
$cmd_args{'dict-pkg'},
$cmd_args{'dict-pkg-args'} || [],
);
note('using dictionary package from --dict-pkg option');
}elsif($rcdata->{default_dictionary}){
if($rcdata->{default_dictionary}->{file}){
$hsxkpwd_args{dictionary_file} = $rcdata->{default_dictionary}->{file};
note('using dictionary file from hsxkpasswdrc file');
}elsif($rcdata->{default_dictionary}->{package}){
$hsxkpwd_args{dictionary} = load_dictionary(
$rcdata->{default_dictionary}->{package},
$rcdata->{default_dictionary}->{package_constructor_args} || [],
);
note('using dictionary package from hsxkpasswdrc file');
}else{
# this should be impossible if the rcfile validation is working!
note('using default word source');
}
}else{
note('using default word source');
}
# deal wit the rng - the commandline takes precedence, then the rc file
if($cmd_args{'rng-pkg'}){
$hsxkpwd_args{rng} = load_rng(
$cmd_args{'rng-pkg'},
$cmd_args{'rng-pkg-args'} || [],
);
note('using rng package from --rng-pkg option');
}elsif($rcdata->{default_rng}){
$hsxkpwd_args{rng} = load_rng(
$rcdata->{default_rng}->{package},
$rcdata->{default_rng}->{package_constructor_args} || [],
);
note('using rng package from hsxkpasswdrc file');
}else{
note('using default rng');
}
#
# === Generate the Password(s) ================================================#
#
# try create an HSXKPasswd object
my $hsxkpwd_obj;
eval{
$hsxkpwd_obj = Crypt::HSXKPasswd->new(%hsxkpwd_args);
1; # explicit evaluation
}or do{
say "ERROR - failed to initialise HSXKPasswd with error:\n* $EVAL_ERROR";
exit 2;
};
# if in verbose mode, print the status of the password generator
if($verbose){
say {*STDERR} "\n".$hsxkpwd_obj->status();
}
# genereate and print the passwords
say join "\n", $hsxkpwd_obj->passwords($num_pwds);
#
# === Helper Functions ========================================================#
#
#####-SUB-######################################################################
# Type : SUBROUTINE
# Purpose : Whine about bad user input and exit
# Returns : nothing, since this function exists the script.
# Arguments : 1) the message to depart with
# Throws : NOTHING
# Notes : Imagine the function name in Queenie from Black Adder's accent :)
# See Also :
sub byeee{
say q{ERROR - }.(shift);
exit 1;
}
#####-SUB-######################################################################
# Type : SUBROUTINE
# Purpose : Print a verbose-mode-only message if appropriate
# Returns : VOID (always returns 1 to keep perlcritic happy)
# Arguments : 1) the message to print
# Throws : NOTHING
# Notes :
# See Also :
sub note{
say {*STDERR} q{*NOTE* }.(shift) if $verbose;
return 1; # to keep perlcritic happy
}
#####-SUB-######################################################################
# Type : SUBROUTINE
# Purpose : Try load an hsxkpasswdrc file
# Returns : A valid RDFileData hashref
# Arguments : 1) the path to try load the data from
# Throws : NOTHING - but does exit the script on error
# Notes : Exits the script with a sane error message via byeee() on error.
# See Also : byeee()
sub load_rcfile{
my $rcfile_path = shift;
# validate the args
unless(NonEmptyString->check($rcfile_path)){
byeee('load_rcfile(): invalid args, must pass a file path as the first argument');
}
unless(-f $rcfile_path){
byeee("file $rcfile_path does not exist");
}
# try slurp the file
open my $RCFILE_FH, '<', $rcfile_path or byee("failed to read file $rcfile_path with error:\n* $OS_ERROR");
my $json_string = do{local $/ = undef; <$RCFILE_FH>};
close $RCFILE_FH;
# try parse the file to JSON
my $loaded_rcdata = {};
eval{
$loaded_rcdata = decode_json($json_string);
1; # explicit evaluation
}or do{
byeee("failed to parse JSON string from file $rcfile_path with error:\n* $EVAL_ERROR");
};
# validate the data structure represnted by the JSON
unless(RCFileData->check($loaded_rcdata)){
byeee("invalid hsxkpasswdrc data from file $rcfile_path:\n* ".RCFileData->get_message($loaded_rcdata));
}
# return the data
return $loaded_rcdata;
}
#####-SUB-######################################################################
# Type : SUBROUTINE
# Purpose : Load a dictionary object form a package name
# Returns : An object of a class that extends Crypt::HSXKPasswd::Dictionary
# Arguments : 1) the Perl package name as a string
# 2) a reference to an array
# Throws : NOTHING - exists the script if there is a problem
# Notes : Exits the script with a sane error message via byeee() on error.
# See Also : byeee()
sub load_dictionary{
my $pkg_name = shift;
my $args_ref = shift;
# validate args
unless(NonEmptyString->check($pkg_name)){
byeee('load_dictionary(): invalid args, must pass a perl package name as the first argument');
}
unless(PerlPackageName->check($pkg_name)){
byeee(PerlPackageName->get_message($pkg_name));
}
unless(ArrayRef->check($args_ref)){
byeee('load_dictionary(): invalid args, must pass a reference to an array as the second argument');
}
# try instantiate an object with the passed class & args
my $dict;
eval{
load $pkg_name;
$dict = $pkg_name->new(@{$args_ref});
1; # explicit evaluation
}or do{
byeee("failed to use '$pkg_name' to initialise a word source with error:\n* $EVAL_ERROR");
};
# return the object
return $dict;
}
#####-SUB-######################################################################
# Type : SUBROUTINE
# Purpose : Load an rng object form a package name
# Returns : An object of a class that extends Crypt::HSXKPasswd::RNG
# Arguments : 1) the Perl package name as a string
# 2) a reference to an array
# Throws : NOTHING - exists the script if there is a problem
# Notes : Exits the script with a sane error message via byeee() on error.
# See Also : byeee()
sub load_rng{
my $pkg_name = shift;
my $args_ref = shift;
# validate args
unless(NonEmptyString->check($pkg_name)){
byeee('load_rng(): invalid args, must pass a perl package name as the first argument');
}
unless(PerlPackageName->check($pkg_name)){
byeee(PerlPackageName->get_message($pkg_name));
}
( run in 1.002 second using v1.01-cache-2.11-cpan-98e64b0badf )