Packages

Ceylan-Myriad, a generic-purpose Erlang toolbox, as an OTP application library here (see http://myriad.esperide.org)

Current section

Files

Jump to
myriad src password_generation.erl
Raw

src/password_generation.erl

% Copyright (C) 2018-2019 Olivier Boudeville
%
% Transferred from generate-password.escript to benefit from a more
% user-friendly debugging.
%
% Author: Olivier Boudeville [olivier (dot) boudeville (at) esperide (dot) com]
%
% Released as LGPL software.
%
-module(password_generation).
% Implementation notes:
%
-define( exec_name, "generate-password.escript" ).
-export([ run/0, main/1 ]).
-type alphabet() :: [ char() ].
-type password() :: text_utils:ustring().
-export([ generate_password/2 ]).
% Typically for testing:
-spec run() -> void().
run() ->
ArgTable = executable_utils:get_argument_table(),
main( ArgTable ).
% Defaults:
-define( default_min_length, "15" ).
-define( default_max_length, "20" ).
-define( default_alphabet, "extended" ).
-spec get_usage() -> basic_utils:void().
get_usage() ->
text_utils:format( "Usage: ~s "
%text_utils:format( "Usage: ~s [-i|--interactive] "
"[-a ALPHABET|--alphabet ALPHABET] "
"[-l MIN_LEN MAX_LEN|--length MIN_LEN MAX_LEN] "
"[-h|--help]~n"
" Generates a suitable password, where:~n"
" - ALPHABET designates the set of characters to draw from "
"(default one being '~s'), among:~n"
" * 'base': alphanumeric letters, all cases [A-Za-z0-9]~n"
" * 'extended': 'base' + basic punctuation (i.e. '~s')~n"
" * 'full': 'base' + all punctuation "
"(i.e. basic + '~s')~n"
" - MIN_LEN and MAX_LEN are the respective minimum and maximum "
"numbers of characters (bounds included) used to "
"generate this password [default: between ~s and ~s]~n",
[ ?exec_name, ?default_alphabet,
get_alphabet( basic_punctuation ), get_alphabet( extra_punctuation ),
?default_min_length, ?default_max_length ] ).
% Sole entry point for this generation service, either triggered by run/0 or by
% the associated escript.
%
-spec main( executable_utils:argument_table() ) -> void().
main( ArgTable ) ->
%trace_utils:debug_fmt( "Original script-specific arguments: ~s",
% [ executable_utils:argument_table_to_string( ArgTable ) ] ),
[ %InteractiveRefKey,
LengthRefKey, AlphaRefKey, HelpRefKey ] =
[ %'-interactive',
'-length', '-alphabet', '-help' ],
% Standardises command-line options:
MergedTable = list_table:merge_in_keys( [
%{ InteractiveRefKey, [ 'i' ] },
{ LengthRefKey, [ 'l' ] },
{ AlphaRefKey, [ 'a' ] },
{ HelpRefKey, [ 'h' ] } ], ArgTable ),
%trace_utils:debug_fmt( "Canonicalized script-specific arguments: ~s",
% [ executable_utils:argument_table_to_string( MergedTable ) ] ),
case list_table:has_entry( HelpRefKey, MergedTable ) of
true ->
display_usage();
false ->
ok
end,
%{ IsInteractive, InterTable } = case list_table:extract_entry_with_defaults(
% InteractiveRefKey, _DefaultInter=false, MergedTable ) of
%
% { [], ShrunkTable } ->
% { true, ShrunkTable };
%
% P={ false, _ShrunkTable } ->
% P
%
%end,
%trace_utils:debug_fmt( "Interactive: ~s", [ IsInteractive ] ),
{ LengthStrings, LenTable } =
list_table:extract_entry_with_defaults(
LengthRefKey,
_LenDefault=[ ?default_min_length, ?default_max_length ],
%InterTable ),
MergedTable ),
{ MinLengthString, MaxLengthString } = case LengthStrings of
[ Min, Max ] ->
{ Min, Max };
Other ->
trace_utils:error( "Error, a minimum and maximum lengths must "
"be specified." ),
throw( { invalid_length_specification, Other } )
end,
MinLength = text_utils:string_to_integer( MinLengthString ),
MaxLength = text_utils:string_to_integer( MaxLengthString ),
case MinLength > MaxLength of
true ->
throw( { invalid_length_order, MinLength, MaxLength } );
false ->
ok
end,
case MinLength > 0 of
true ->
ok;
false ->
throw( { invalid_minimum_length, MinLength } )
end,
%trace_utils:debug_fmt( "Min length: ~B", [ MinLength ] ),
%trace_utils:debug_fmt( "Max length: ~B", [ MaxLength ] ),
{ [ AlphabetStringSpec ], AlphaTable } =
list_table:extract_entry_with_defaults( AlphaRefKey,
_AlphaDefault=[ ?default_alphabet ], LenTable ),
AlphabetSpec = text_utils:string_to_atom( AlphabetStringSpec ),
%trace_utils:debug_fmt( "Alphabet spec: ~s", [ AlphabetSpec ] ),
[ Length ] =
random_utils:get_random_values( MinLength, MaxLength, _Count=1 ),
case list_table:keys( AlphaTable ) of
[] ->
ok;
UnexpectedOpts ->
trace_utils:error_fmt(
"Unexpected command-line option(s) specified: ~s",
[ text_utils:atoms_to_string( UnexpectedOpts ) ] )
end,
Alphabet = get_alphabet( AlphabetSpec ),
%trace_utils:debug_fmt( "Input alphabet corresponding to spec ~p: "
% "'~w' (i.e. '~s').",
% [ AlphabetSpec, Alphabet, Alphabet ] ),
Password = generate_password( Alphabet, Length ),
io:format( "Generated password (alphabet spec: ~s, length: ~B) is: "
"~s ~n", [ AlphabetSpec, Length, Password ] ),
basic_utils:stop( _ErrorCode=0 ).
% Displays the usage of this service, and stops (with no error).
display_usage() ->
io:format( get_usage(), [] ),
basic_utils:stop( _ErrorCode=0 ).
% Returns the corresponding alphabet, based on its spec, expressed as an atom
% (ex: 'numeric' for all numeric literals) or as a list thereof.
%
get_alphabet( AlphabetSpecs ) when is_list( AlphabetSpecs ) ->
list_utils:flatten_once( [ get_alphabet( A ) || A <- AlphabetSpecs ] );
get_alphabet( _AlphabetSpec=base ) ->
get_alphabet( [ lower_case, upper_case, numeric ] );
get_alphabet( _AlphabetSpec=extended ) ->
get_alphabet( [ lower_case, upper_case, numeric, basic_punctuation ] );
get_alphabet( _AlphabetSpec=full ) ->
get_alphabet( [ lower_case, upper_case, numeric, basic_punctuation,
extra_punctuation ] );
get_alphabet( _AlphabetSpec=lower_case ) ->
lists:seq( $a, $z );
get_alphabet( _AlphabetSpec=upper_case ) ->
lists:seq( $A, $Z );
get_alphabet( _AlphabetSpec=numeric ) ->
lists:seq( $0, $9 );
get_alphabet( _AlphabetSpec=basic_punctuation ) ->
[ $[, $], $(, $), ${, $}, $:, $,, $;, $-, $_, $., $!, $? ];
get_alphabet( _AlphabetSpec=extra_punctuation ) ->
[ $", $', $@, $ , $/, $&, $$, $*, $\\, $^, $%, $=, $+, $| ].
% Generates a password of specified exact length, from specified alphabet.
-spec generate_password( alphabet(), basic_utils:count() ) -> password().
generate_password( Alphabet, CharCount ) ->
% Of course we do not want a reproducible seeding:
random_utils:start_random_source( time_based_seed ),
AlphaSize = length( Alphabet ),
%trace_utils:debug_fmt( "Alphabet size: ~B", [ AlphaSize ] ),
generate_helper( CharCount, Alphabet, AlphaSize, _Acc=[] ).
% (helper)
generate_helper( _CharCount=0, _Alphabet, _AlphaSize, Acc ) ->
% No order matters, no reverse useful:
Acc;
generate_helper( CharCount, Alphabet, AlphaSize, Acc ) ->
NewCharIndex = random_utils:get_random_value( AlphaSize ),
NewChar = list_utils:get_element_at( Alphabet, NewCharIndex ),
%trace_utils:debug_fmt( "Drawn '~B' (~s), at index #~B",
% [ NewChar, [ NewChar ], NewCharIndex ] ),
generate_helper( CharCount-1, Alphabet, AlphaSize, [ NewChar | Acc ] ).