File Coverage

blib/lib/Concierge/Auth/Generators.pm
Criterion Covered Total %
statement 53 65 81.5
branch 15 26 57.6
condition 17 22 77.2
subroutine 9 12 75.0
pod 5 9 55.5
total 99 134 73.8


line stmt bran cond sub pod time code
1             package Concierge::Auth::Generators v0.4.3;
2 6     6   3281 use v5.36;
  6         20  
3              
4             # ABSTRACT: Value generation utilities for Concierge::Auth
5              
6 6     6   2627 use Crypt::PRNG qw/rand random_bytes random_string random_string_from/;
  6         21434  
  6         488  
7 6     6   42 use Exporter 'import';
  6         15  
  6         6632  
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 35     35 0 55 my $value = shift;
33 35   50     66 my $message = shift || "Generation successful.";
34 35 100       174 wantarray ? ($value, $message) : $value;
35             }
36              
37             sub g_error {
38 0   0 0 0 0 my $message = shift || "Generation failed.";
39 0 0       0 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 221856 my @bytes = unpack('C*', random_bytes(16));
46              
47             # Set version 4 (bits 12-15 of time_hi_and_version)
48 4         166 $bytes[6] = ($bytes[6] & 0x0f) | 0x40;
49             # Set variant 1 (bits 6-7 of clock_seq_hi_and_reserved)
50 4         6 $bytes[8] = ($bytes[8] & 0x3f) | 0x80;
51              
52 4         46 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         9 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 4216 my $bytes = shift || 20;
64 8 100       48 $bytes = $bytes =~ /^\d+$/ ? $bytes : 20;
65              
66 8         45 my $id = unpack('H*', random_bytes($bytes));
67 8         102 return g_success($id, "Random ID generated ($bytes bytes).");
68             }
69              
70             ## gen_token: deprecated alias for gen_random_token
71             sub gen_token {
72 0     0 0 0 goto &gen_random_token;
73             }
74              
75             ## gen_random_token: generate random alphanumeric token
76             ## use random_string() from Crypt::PRNG
77             ## Parameters: length (optional, default 13)
78             ## Returns: random string of specified length
79             sub gen_random_token {
80 5   100 5 1 3022 my $length = shift || 13;
81 5 50       48 $length = $length =~ /^\d+$/ ? $length : 13;
82              
83 5         13 my $token = random_string($length);
84 5         671 return g_success($token, "Random token generated ($length chars).");
85             }
86              
87             ## gen_crypt_token: deprecated, now an alias for gen_random_token
88             sub gen_crypt_token {
89 0     0 0 0 goto &gen_random_token;
90             }
91              
92             ## gen_random_string: generate random string from optional charset
93             ## uses random_string() and random_string_from() from Crypt::PRNG
94             ## Parameters: length, charset (optional)
95             ## If charset provided and not empty, uses random_string_from
96             ## Otherwise uses alphanumeric charset
97             ## Returns: random string of specified length
98             sub gen_random_string {
99 15     15 1 2635 my ($length, $charset) = @_;
100 15 100 66     97 $length = (defined $length and $length =~ /^\d+$/) ? $length : 13;
101              
102 15 100 66     52 my $string = ($charset && $charset !~ /^\s*$/)
103             ? random_string_from($charset, $length)
104             : random_string($length);
105              
106 15         1393 return g_success($string, "Random string generated ($length chars).");
107             }
108              
109             ## gen_word_phrase: generate multi-word passphrase from dictionary
110             ## Parameters:
111             ## num_words: number of words (default 4)
112             ## min_chars: minimum word length (default 4)
113             ## max_chars: maximum word length (default 7)
114             ## word_sep: separator between words (default '')
115             ## Returns: passphrase string or fallback random phrase
116             sub gen_word_phrase {
117 3   100 3 1 1860 my $num_words = shift || 4;
118 3   100     10 my $min_chars = shift || 4;
119 3   100     9 my $max_chars = shift || 7;
120 3   100     11 my $word_sep = shift || '';
121              
122 3         5 my $word_file = '/usr/share/dict/web2';
123 3         5 my @wordlist;
124 3         5 my $used_fallback = 0;
125              
126 3 50       1107 if (open my $wfh, "<", $word_file) {
127 0         0 FILE: while (<$wfh>) {
128 0         0 my $line = $_;
129 0         0 chomp $line;
130 0 0       0 next unless length($line) > $min_chars - 1;
131 0 0       0 next if length($line) > $max_chars;
132 0         0 push @wordlist => $line;
133             }
134 0         0 close $wfh;
135             }
136             else {
137             # Fallback: generate random "words"
138 3         8 $used_fallback = 1;
139 3         11 for (1..$num_words) {
140 11         31 my $length = $min_chars + int(rand($max_chars - $min_chars + 1));
141 11         152 my ($word, $msg) = gen_random_string($length);
142 11         32 push @wordlist => lc $word;
143             }
144             }
145              
146 3         7 my $list_size = scalar @wordlist;
147 3 50       8 if ($list_size == 0) {
148 0         0 return g_error("No words available for phrase generation.");
149             }
150              
151 3         6 my @rand;
152             my %seen;
153              
154 3         8 WORD: while (scalar @rand < $num_words) {
155 18         32 my $num = int(rand($list_size));
156 18 100       167 next WORD if $seen{$num}++;
157 11         20 my $wd = $wordlist[$num];
158 11 50       27 next WORD if $wd =~ /^[A-Z]/;
159 11         33 push @rand => ucfirst $wd;
160             }
161              
162 3         12 my $phrase = join $word_sep => @rand;
163              
164 3 50       9 my $msg = $used_fallback
165             ? "Word phrase generated (fallback mode)."
166             : "Word phrase generated from dictionary.";
167              
168 3         7 return g_success($phrase, $msg);
169             }
170              
171             1;
172              
173             __END__