File Coverage

blib/lib/Concierge/Auth/Generators.pm
Criterion Covered Total %
statement 57 65 87.6
branch 18 26 69.2
condition 19 22 86.3
subroutine 12 12 100.0
pod 5 9 55.5
total 111 134 82.8


line stmt bran cond sub pod time code
1             package Concierge::Auth::Generators v0.4.5;
2 6     6   2714 use v5.36;
  6         18  
3              
4             # ABSTRACT: Value generation utilities for Concierge::Auth
5              
6 6     6   2098 use Crypt::PRNG qw/rand random_bytes random_string random_string_from/;
  6         19915  
  6         395  
7 6     6   38 use Exporter 'import';
  6         7  
  6         4775  
8              
9             our @EXPORT_OK = qw(
10             gen_uuid
11             gen_random_id
12             gen_random_token
13             gen_random_string
14             gen_word_phrase
15             );
16              
17             our %EXPORT_TAGS = (
18             str => [qw/gen_random_string gen_word_phrase/],
19             rand => [qw/gen_random_id gen_random_token gen_random_string/],
20             tok => [qw/gen_random_id gen_random_token gen_uuid/],
21             all => [qw/gen_uuid gen_random_id gen_random_token gen_random_string gen_word_phrase/],
22             );
23              
24             ## Generator response methods
25             ## These provide non-fatal error handling in generator functions
26             ##
27             ## Usage:
28             ## return g_success($value, $message) # Success with value
29             ## return g_error($message) # Failure with undef
30              
31             sub g_success {
32 51     51 0 3974 my $value = shift;
33 51   100     112 my $message = shift || "Generation successful.";
34 51 100       190 wantarray ? ($value, $message) : $value;
35             }
36              
37             sub g_error {
38 2   50 2 0 1038 my $message = shift || "Generation failed.";
39 2 100       9 wantarray ? (undef, $message) : undef;
40             }
41              
42             ## gen_uuid: generate a version 4 (random) UUID using Crypt::PRNG
43             ## Returns: UUID string (e.g., "550e8400-e29b-41d4-a716-446655440000")
44             sub gen_uuid {
45 4     4 1 148369 my @bytes = unpack('C*', random_bytes(16));
46              
47             # Set version 4 (bits 12-15 of time_hi_and_version)
48 4         420 $bytes[6] = ($bytes[6] & 0x0f) | 0x40;
49             # Set variant 1 (bits 6-7 of clock_seq_hi_and_reserved)
50 4         7 $bytes[8] = ($bytes[8] & 0x3f) | 0x80;
51              
52 4         23 my $uuid = sprintf(
53             '%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x',
54             @bytes
55             );
56 4         12 return g_success($uuid, "UUID v4 generated.");
57             }
58              
59             ## gen_random_id: generate a random hex ID from cryptographic random bytes
60             ## Parameters: byte_length (optional, default 20 = 40 hex chars)
61             ## Returns: hex-encoded random string
62             sub gen_random_id {
63 8   100 8 1 5731 my $bytes = shift || 20;
64 8 100       44 $bytes = $bytes =~ /^\d+$/ ? $bytes : 20;
65              
66 8         21 my $id = unpack('H*', random_bytes($bytes));
67 8         138 return g_success($id, "Random ID generated ($bytes bytes).");
68             }
69              
70             ## gen_random_token: generate random alphanumeric token
71             ## use random_string() from Crypt::PRNG
72             ## Parameters: length (optional, default 13)
73             ## Returns: random string of specified length
74             sub gen_random_token {
75 10   100 10 1 5940 my $length = shift || 13;
76 10 100       55 $length = $length =~ /^\d+$/ ? $length : 13;
77              
78 10         40 my $token = random_string($length);
79 10         1372 return g_success($token, "Random token generated ($length chars).");
80             }
81              
82             ## gen_random_string: generate random string from optional charset
83             ## uses random_string() and random_string_from() from Crypt::PRNG
84             ## Parameters: length, charset (optional)
85             ## If charset provided and not empty, uses random_string_from
86             ## Otherwise uses alphanumeric charset
87             ## Returns: random string of specified length
88             sub gen_random_string {
89 22     22 1 3212 my ($length, $charset) = @_;
90 22 100 66     103 $length = (defined $length and $length =~ /^\d+$/) ? $length : 13;
91              
92 22 100 66     61 my $string = ($charset && $charset !~ /^\s*$/)
93             ? random_string_from($charset, $length)
94             : random_string($length);
95              
96 22         1671 return g_success($string, "Random string generated ($length chars).");
97             }
98              
99             ## gen_word_phrase: generate multi-word passphrase from dictionary
100             ## Parameters:
101             ## num_words: number of words (default 4)
102             ## min_chars: minimum word length (default 4)
103             ## max_chars: maximum word length (default 7)
104             ## word_sep: separator between words (default '')
105             ## Returns: passphrase string or fallback random phrase
106             sub gen_word_phrase {
107 5   100 5 1 4658 my $num_words = shift || 4;
108 5   100     15 my $min_chars = shift || 4;
109 5   100     12 my $max_chars = shift || 7;
110 5   100     14 my $word_sep = shift || '';
111              
112 5         7 my $word_file = '/usr/share/dict/web2';
113 5         6 my @wordlist;
114 5         7 my $used_fallback = 0;
115              
116 5 50       316 if (open my $wfh, "<", $word_file) {
117 0         0 FILE: while (<$wfh>) {
118 0         0 my $line = $_;
119 0         0 chomp $line;
120 0 0       0 next unless length($line) > $min_chars - 1;
121 0 0       0 next if length($line) > $max_chars;
122 0         0 push @wordlist => $line;
123             }
124 0         0 close $wfh;
125             }
126             else {
127             # Fallback: generate random "words"
128 5         10 $used_fallback = 1;
129 5         15 for (1..$num_words) {
130 18         37 my $length = $min_chars + int(rand($max_chars - $min_chars + 1));
131 18         160 my ($word, $msg) = gen_random_string($length);
132 18         42 push @wordlist => lc $word;
133             }
134             }
135              
136 5         7 my $list_size = scalar @wordlist;
137 5 50       11 if ($list_size == 0) {
138 0         0 return g_error("No words available for phrase generation.");
139             }
140              
141 5         6 my @rand;
142             my %seen;
143              
144 5         13 WORD: while (scalar @rand < $num_words) {
145 44         53 my $num = int(rand($list_size));
146 44 100       256 next WORD if $seen{$num}++;
147 18         18 my $wd = $wordlist[$num];
148 18 50       34 next WORD if $wd =~ /^[A-Z]/;
149 18         31 push @rand => ucfirst $wd;
150             }
151              
152 5         13 my $phrase = join $word_sep => @rand;
153              
154 5 50       9 my $msg = $used_fallback
155             ? "Word phrase generated (fallback mode)."
156             : "Word phrase generated from dictionary.";
157              
158 5         9 return g_success($phrase, $msg);
159             }
160              
161             ## gen_token: deprecated alias for gen_random_token
162             sub gen_token {
163 1     1 0 3401 goto &gen_random_token;
164             }
165              
166             ## gen_crypt_token: deprecated, now an alias for gen_random_token
167             sub gen_crypt_token {
168 1     1 0 2231 goto &gen_random_token;
169             }
170              
171              
172             1;
173              
174             __END__