File Coverage

blib/lib/String/Obfuscate.pm
Criterion Covered Total %
statement 78 87 89.6
branch 15 26 57.6
condition 8 11 72.7
subroutine 15 17 88.2
pod 7 11 63.6
total 123 152 80.9


line stmt bran cond sub pod time code
1 2     2   481593 use v5.20;
  2         6  
2 2     2   8 use warnings;
  2         3  
  2         129  
3 2     2   468 use experimental qw(signatures postderef);
  2         1182  
  2         10  
4              
5             package String::Obfuscate 0.01 {
6 2     2   1368 use Math::Random::ISAAC ();
  2         2289  
  2         145  
7 2     2   28 use constant STD_CHARS => ['a'..'z', 'A'..'Z', 0..9];
  2         3  
  2         2114  
8              
9             my $pp_shuffle = eval {
10             require List::Util::XS;
11             $List::Util::XS::VERSION >= 1.54;
12             } ? undef : sub ($rand_func, @array) {
13             for (my $idx = scalar @array; $idx > 1;) {
14             my $swap_idx = int($rand_func->() * $idx--);
15             my $tmp_val = $array[$swap_idx];
16             $array[$swap_idx] = $array[$idx];
17             $array[$idx] = $tmp_val;
18             }
19             return @array;
20             };
21              
22 1158     1158 1 29497 sub new ($class, %params) {
  1158         1539  
  1158         2503  
  1158         1193  
23 1158         1729 my $seed = delete $params{'seed'}; # optional seed
24 1158         1730 my $chars = delete $params{'chars'}; # optional char list
25 1158         1541 my $passph = delete $params{'passphrase'};
26 1158         1557 my $rtn_src = delete $params{'retain_source'};
27              
28 1158 50       2402 die 'unexpected param(s): ' . join(', ', keys %params)
29             if keys %params;
30 1158 50 66     4046 die 'cannot use both a seed and a passphrase'
31             if defined $seed and defined $passph;
32 1158 50 66     3027 die 'chars param must be a reference'
33             if defined $chars and not ref $chars;
34              
35 1158 50 66     3666 $chars = [ split '', $$chars ] if $chars and ref $chars eq 'SCALAR';
36 1158 100       1706 $seed = [length $passph, unpack('L*', $passph)] if $passph;
37 1158 100       1849 $seed = make_seed() if !defined $seed;
38 1158 100       2954 $seed = [$seed] if !ref $seed;
39              
40 1158 50 100     4354 my $self = bless {
41             chars => $chars // STD_CHARS,
42             seed => $seed,
43             $rtn_src ? (rtn_src => !!1) : (),
44             }, $class;
45              
46 1158         2526 $self->make_codec;
47 1158         3292 return $self;
48             }
49              
50 1158     1158 1 1261 sub chars_shuffled ($self) {
  1158         1195  
  1158         1144  
51 1158         1593 my $rng = Math::Random::ISAAC->new($self->seed->@*);
52 1158     258885   24950 my $rand_fn = sub { $rng->rand() };
  258885         1050017  
53 1158         1909 my @chars_s = my_shuffle($rand_fn, $self->chars);
54 1158         74102 return \@chars_s;
55             }
56              
57 1158     1158 0 1250 sub make_codec ($self) {
  1158         1408  
  1158         1288  
58 1158         1879 my $fr_chars = quotemeta(join '', $self->chars->@* );
59 1158         2162 my $to_chars = quotemeta(join '', $self->chars_shuffled->@*);
60              
61 1158         10679 my ($enc_src, $dec_src);
62              
63 1158 50       213151 $self->{encode} = eval($enc_src = qq`sub { \$_[0] =~ tr|$fr_chars|$to_chars|r }`) or die $@;
64 1158 50       245365 $self->{decode} = eval($dec_src = qq`sub { \$_[0] =~ tr|$to_chars|$fr_chars|r }`) or die $@;
65              
66 1158 50       6475 $self->{src} = [$enc_src, $dec_src] if $self->{rtn_src};
67 1158         2183 return;
68             }
69              
70 1158     1158 0 1262 sub my_shuffle ($rand_func, $arrayref) {
  1158         1073  
  1158         1109  
  1158         1088  
71 1158 50       1888 return $pp_shuffle->($rand_func, @$arrayref) if $pp_shuffle;
72 1158         1430 local $List::Util::RAND = $rand_func;
73 1158         6408 return List::Util::shuffle(@$arrayref);
74             }
75              
76 0     0 1 0 sub dump_source ($self) {
  0         0  
  0         0  
77 0 0       0 unless ($self->{src}) {
78 0         0 $self->{rtn_src} = 1;
79 0         0 $self->make_codec;
80             }
81 0         0 return @{$self->{src}};
  0         0  
82             }
83              
84 28     28 0 40 sub make_seed () { [time(), $$] }
  28         27  
  28         187  
85 1251     1251 1 24251 sub seed ($self) { $self->{'seed'} }
  1251         1243  
  1251         1176  
  1251         4057  
86 2316     2316 1 2434 sub chars ($self) { $self->{chars} }
  2316         2362  
  2316         2281  
  2316         14006  
87 1165     1165 1 3800 sub obfuscate ($self, $string) { $self->{encode}->($string) }
  1165         1707  
  1165         1519  
  1165         1285  
  1165         21719  
88 1138     1138 1 5973 sub deobfuscate ($self, $string) { $self->{decode}->($string) }
  1138         1249  
  1138         1381  
  1138         1160  
  1138         17493  
89 0     0 0   sub using_list_util_xs { not $pp_shuffle }
90             }
91              
92             1;
93              
94             =head1 NAME
95              
96             String::Obfuscate - Reversibly obfuscate a string with a substitution cipher.
97              
98              
99             =head1 VERSION
100              
101             version 0.01
102              
103              
104             =head1 SYNOPSIS
105              
106             use String::Obfuscate;
107             my $obf = String::Obfuscate->new(seed => 123);
108             $obf->obfuscate('hello'); # 'xn88Y'
109             $obf->deobfuscate('xn88Y'); # 'hello'
110              
111              
112             =head1 DESCRIPTION
113              
114             String::Obfuscate implements a substitution type cipher adequate to obfuscate
115             a string without being cryptographically secure. The cipher mapping is
116             dynamically generated based on a seed or seeds which are fed to a random number
117             generator.
118              
119             Specify seed(s) yourself to get a predictable result. Otherwise, the order will
120             be different with each String::Obfuscate object, but obfuscated strings can
121             still be reversed with the same object, or by asking the object for the seed and
122             and re-using the same seed.
123              
124             If no seed is supplied, this module will create one based on the time and PID,
125             however this method may change in the future.
126              
127             Randomness is supplied by the Math::Random::ISAAC, module which has both XS
128             and pure-perl implementations. This has several advantages:
129             - The XS module is very fast while the PP module can be used as a fallback
130             - Using a discrete RNG prevents alterating the state of perl's built-in RNG
131             - The same algorithm can be implemented in another language if desired
132              
133             If version 1.54 or greater of List::Util::XS is not available, a pure-perl
134             implementation of the same shuffle algorithm will be used (not List::Util::PP
135             which uses a different shuffle algorithm). Again, this ensures reproducibility.
136              
137             Only ASCII letters and numbers are scrambled, but you can specify your own
138             character set to the new constructor with the chars param, which takes a
139             reference (to a string or an array of characters). This is done to prevent
140             excessive string copying and for a possible future feature where a plain string
141             might have a special meaning, such as the name of a character set.
142              
143             Internally, this module generates a pair of encoding/decoding subroutines that
144             use a translation regex. Once the object is created, encoding and decoding is
145             very fast. However, if desired, you can dump the source code of the generated
146             subroutines/regexes.
147              
148             Included in this distribution are String::Obfuscate::Base64 and
149             String::Obfuscate::Base64::URL which will convert the string to base 64 using
150             the standard or URL encoding, respectively, then obfuscate it. These subclasses
151             do not let you specify a character set. If the string you desire to obfuscate
152             contains binary data or UTF-8 characters, it is recommended you use one of
153             these Base64 subclasses.
154              
155              
156             =head1 REQUIREMENTS
157              
158             Math::Random::ISAAC (::XS or ::PP)
159              
160             perl v5.20 or greater
161              
162             A minimum perl version of 5.20 is required as this module uses subroutine
163             signatures and postfix dereferencing. As of this writing, this version is
164             approximately 12 years old. You are encouraged to upgrade.
165              
166              
167             =head1 RECOMMENDATIONS
168              
169             List::Util::XS version 1.54 or greater
170              
171             Older versions of List::Util do not allow you to specify a custom RNG.
172              
173              
174             =head1 RATIONALE
175              
176             This module can be used to obscure non-security-sensitive data in a way that
177             is several orders of magnitude faster than encrypting it, while using a more
178             complex cipher than one with a fixed rotation (such as Crypt::Cipher::Rot47,
179             which is only slightly faster than this module).
180              
181              
182             =head1 CONSTRUCTOR
183              
184             =over 4
185              
186             =item B
187              
188             Returns a new L object constructed according to PARAMS,
189             where PARAMS are name/value pairs. All PARAMS are optional. If a seed is not
190             specified, one will be created.
191              
192             $ob = String::Obfuscate->new;
193             $ob = String::Obfuscate->new(seed => 123);
194             $ob = String::Obfuscate->new(chars => ['a'..'f',0..9]);
195             $ob = String::Obfuscate->new(passphrase => 'abcdefg');
196              
197             =item chars
198              
199             The characters used to generate the cipher, specified as an arrayref or stringref.
200              
201             =item seed
202              
203             The seed or seed(s). May be specified as a number or an arrayref of multiple
204             seeds. The random number generator can take up to 255 seeds.
205              
206             =item passphrase
207              
208             Instead of specifying a seed, you can specify a string passphrase which will
209             be converted to a series of seeds. The first seed is the length of the string,
210             then four-character groups are converted to 32-bit integers using unpack.
211              
212             =item retain_source
213              
214             Set to a true value, the source code of the generated encoding/decoding
215             subroutines will be saved before being eval-ed.
216              
217             =back
218              
219              
220             =head1 OBJECT METHODS
221              
222             =over 4
223              
224             =item B
225              
226             Returns the seed. Regardless of how the seed was originally supplied, this
227             method will always return an arrayref.
228              
229             Note the seed is set at object creation and cannot be changed later.
230              
231             =item B
232              
233             =item B
234              
235             Returns the source or destination character list as an arrayref.
236              
237             These are set at object creation and cannot be changed later.
238              
239             =item B
240              
241             Returns a two-element array. The first element is a string representation of
242             the obfuscation subroutine; the second element is the deobfuscation subroutine.
243             If retain_source was not passed to new(), this method can still be called, but
244             the subroutines will be re-generated.
245              
246             =item B
247              
248             Returns the obfuscated version of $string without altering the original.
249              
250             =item B
251              
252             Returns the deobfuscated version of $string without altering the original.
253              
254             =back
255              
256              
257             =head1 AUTHOR
258              
259             Dondi Michael Stroma
260              
261              
262             =head1 COPYRIGHT
263              
264             Copyright (C) 2025 by Dondi Michael Stroma. All rights reserved.
265              
266              
267             =head1 LICENSE
268              
269             This program is free software; you can redistribute it and/or modify it under
270             the same terms as Perl itself.