File Coverage

blib/lib/Crypt/SecretBuffer.pm
Criterion Covered Total %
statement 152 218 69.7
branch 79 166 47.5
condition 34 97 35.0
subroutine 18 18 100.0
pod 6 7 85.7
total 289 506 57.1


line stmt bran cond sub pod time code
1             package Crypt::SecretBuffer;
2             # VERSION
3             # ABSTRACT: Prevent accidentally leaking a string of sensitive data
4             $Crypt::SecretBuffer::VERSION = '0.024';
5              
6 26     26   6672808 use strict;
  26         61  
  26         1077  
7 26     26   152 use warnings;
  26         63  
  26         1397  
8 26     26   153 use Carp;
  26         58  
  26         1654  
9 26     26   3828 use IO::Handle;
  26         39931  
  26         1444  
10 26     26   215 use Scalar::Util ();
  26         75  
  26         700  
11 26     26   130 use Time::HiRes;
  26         47  
  26         259  
12 26     26   1291 use Fcntl ();
  26         148  
  26         805  
13 26     26   2486 use parent qw( DynaLoader );
  26         1682  
  26         230  
14 26         325 use overload '""' => \&stringify,
15 26     26   2900 'cmp' => \&memcmp;
  26         49  
16              
17 26     26 1 23309 sub dl_load_flags {0x01} # Share extern symbols with other modules
18             bootstrap Crypt::SecretBuffer;
19              
20              
21             {
22             package Crypt::SecretBuffer::Exports;
23             $Crypt::SecretBuffer::Exports::VERSION = '0.024';
24 26     26   4231 use Exporter 'import';
  26         53  
  26         86767  
25             @Crypt::SecretBuffer::Exports::EXPORT_OK= qw(
26             secret_buffer secret span unmask_secrets_to memcmp
27             NONBLOCK AT_LEAST ISO8859_1 ASCII UTF8 UTF16LE UTF16BE HEX BASE64
28             MATCH_MULTI MATCH_REVERSE MATCH_NEGATE MATCH_ANCHORED MATCH_CONST_TIME
29             _wait_fh_readable
30             );
31             }
32              
33             # Some of the exported functions are not methods, so instead of having them in the object's
34             # namespace I put them in the ::Exports namespace. Importing from Crypt::SecretBuffer is
35             # equivalent to importing from Crypt::SecretBuffer::Exports.
36             sub import {
37 55     55   411 splice(@_, 0, 1, 'Crypt::SecretBuffer::Exports');
38 55         203406 goto \&Crypt::SecretBuffer::Exports::import;
39             }
40              
41             # For "use Inline -with => 'Crypt::SecretBuffer';" but lazy-load the data.
42             sub Inline {
43 1     1 0 269417 require Crypt::SecretBuffer::Install::Files;
44 1         8 goto \&Crypt::SecretBuffer::Install::Files::Inline;
45             }
46              
47              
48             sub stringify_mask {
49 4     4 1 459961 my $self= shift;
50 4 100       14 if (@_) {
51 3         91 $self->{stringify_mask}= shift;
52 3         17 return $self;
53             }
54             $self->{stringify_mask}
55 1         4 }
56              
57              
58             sub append_console_line {
59 47     47 1 3933139 my $self= shift;
60 47         103 my %options;
61              
62             # First argument can be input_fh, or just straight key/value list.
63 47 100 66     1038 if (@_ && ref($_[0]) && (ref $_[0] eq 'GLOB' || ref($_[0])->can('getc'))) {
      33        
      66        
64 42 50       243 croak "Expected even-length list of options" unless @_ & 1;
65 42         401 %options= (input_fh => @_);
66             } else {
67 5 50       76 croak "Expected even-length list of options" if @_ & 1;
68 5         138 %options= @_;
69             }
70              
71             # This routine can resume where it left off by keeping most variables in %state
72 47   100     316 my $state= delete $options{state} || {};
73              
74             # If 'start_len' is set, it means the prompt was already started and this is a continuation.
75             # The prompt is only printed on the initial call, unless they set 're_prompt'.
76 47 100       247 my $print_prompt= defined $state->{start_len}? delete $state->{re_prompt} : defined $options{prompt};
77             $state->{start_len}= $self->length
78 47 100       512 unless defined $state->{start_len};
79              
80             # copy new parameters into state hash
81 47         236 for (qw( input_fh prompt prompt_fh char_mask char_count char_max char_class utf8 )) {
82             $state->{$_}= delete $options{$_}
83 376 100       4104 if exists $options{$_};
84             }
85             # the default char_class for utf8 is the set of valid printable characters
86             $state->{char_class} ||= qr/[\p{Print}]/
87 47 100 33     364 if $state->{utf8};
88              
89             # local vars for convenience
90             my ($input_fh, $prompt_fh, $char_mask, $char_count, $char_max, $char_class, $start_len, $utf8)
91 47         137 = @{$state}{qw( input_fh prompt_fh char_mask char_count char_max char_class start_len utf8)};
  47         241  
92              
93             # timeout is per-call
94 47         115 my $timeout= delete $options{timeout};
95              
96 47 50       140 warn "unknown option: ".join(', ', keys %options)
97             if keys %options;
98              
99             # Resolve input file handle if not specified
100 47 50       139 if (!defined $input_fh) {
101             # undef is a request to read from the controlling terminal
102 0 0       0 if ($^O eq 'MSWin32') {
103 0 0       0 open $input_fh, '+<', 'CONIN$' or croak 'open(CONIN$): '.$!;
104 0         0 $state->{reading_from}= 'CONIN$';
105 0 0       0 unless (defined $state->{prompt_fh}) {
106 0 0       0 open $prompt_fh, '>', 'CONOUT$' or croak 'open(CONOUT$): '.$!;
107 0         0 $state->{prompt_fh}= $prompt_fh;
108 0         0 $state->{writing_to}= 'CONOUT$';
109             }
110             } else {
111 0 0       0 open $input_fh, '+<', '/dev/tty' or croak "open(/dev/tty): $!";
112 0         0 $state->{reading_from}= '/dev/tty';
113 0 0       0 unless (defined $state->{prompt_fh}) {
114 0         0 $state->{prompt_fh}= $state->{input_fh};
115 0         0 $state->{writing_to}= '/dev/tty';
116             }
117             }
118 0         0 $state->{input_fh}= $input_fh;
119             } else {
120 47   100     392 $state->{reading_from} ||= 'supplied_handle';
121             }
122              
123             # Resolve prompt file handle if required and not specified
124 47 100 66     267 if (defined $state->{prompt} || defined $char_mask) {
125 5 50       29 if (!defined $prompt_fh) {
126             # Determine default prompt_fh
127             # For terminals, if it was STDIN then the underlying descriptors or libc FILE handle
128             # are probably read-only, so open a new writeable handle. Also MSWin32 only has one
129             # console, so do this even if it isn't currently set as STDIN.
130 0         0 my $fd= fileno($input_fh);
131 0 0 0     0 if (-t $input_fh && ((defined $fd && $fd == 0) || \*STDIN == $input_fh || $^O eq 'MSWin32')) {
    0 0        
      0        
132 0 0       0 if ($^O eq 'MSWin32') {
133 0 0       0 open $prompt_fh, '>', 'CONOUT$' or croak 'open(CONOUT$): '.$!;
134 0         0 $state->{writing_to}= 'CONOUT$';
135             } else {
136 0 0       0 open $prompt_fh, '+<', '/dev/tty' or croak "open(/dev/tty): $!";
137 0         0 $state->{writing_to}= '/dev/tty';
138             }
139 0         0 $state->{prompt_fh}= $prompt_fh;
140             }
141             # For sockets or tty, default to the same file descriptor as input_fh.
142             # If the descriptor is read-only, things will fail, and it's the caller's
143             # job to fix the bug.
144             elsif (-S $input_fh || -t $input_fh) {
145 0         0 $state->{prompt_fh}= $prompt_fh= $input_fh;
146 0         0 $state->{writing_to}= 'input handle';
147             }
148             # Suppress prompt unless the handle looks like a TTY or Socket. e.g. input from file
149             # or pipe can't usefully be prompted. It could be that the parent process created a
150             # return pipe on STDOUT and wants to see the prompt there, but it would be too bold to
151             # take a guess at that. The caller can supply prompt_fh => \*STDOUT if they want to.
152             else {
153 0         0 delete @{$state}{qw( prompt char_mask )};
  0         0  
154             }
155             } else {
156 5   100     114 $state->{writing_to} ||= 'supplied_handle';
157             }
158             }
159              
160             # If the user wants control over the keypresses, need to disable line-editing mode.
161             # ConsoleState obj with auto_restore restores the console state when it goes out of scope.
162 47   66     547 my $input_by_chars= defined $char_mask || defined $char_count
163             || defined $char_class || defined $timeout
164             || $utf8;
165 47   33     1124 $state->{ttystate} ||= Crypt::SecretBuffer::ConsoleState->maybe_new(handle => $input_fh);
166 47         105 my $ttystate= $state->{ttystate};
167 47 50 33     192 if ($ttystate && $ttystate->echo) {
168 0         0 $ttystate->auto_restore(1);
169 0         0 $ttystate->echo(0);
170             }
171 47 0 33     120 if ($ttystate && $ttystate->line_input && $input_by_chars) {
      33        
172 0         0 $ttystate->auto_restore(1);
173 0         0 $ttystate->line_input(0);
174             }
175             # Write the initial prompt
176 47 100       127 if ($print_prompt) {
177 1         10 my $suffix= '';
178 1 50 33     18 if (defined $start_len && defined $char_mask) {
179 0         0 my $n= $self->len - $start_len;
180 0 0 0     0 if ($n && $utf8) {
181 0         0 $n= 0;
182 0         0 my $span= $self->span(pos => $start_len, encoding => 'UTF-8');
183 0   0     0 ++$n while $span->len && eval { $span->ltrim(qr/./) };
  0         0  
184             }
185 0         0 $suffix= $char_mask x $n;
186             }
187 1 50 33     22 $prompt_fh->print($state->{prompt} . $suffix) && $prompt_fh->flush
188             or croak "Failed to write $state->{writing_to}: $!";
189             }
190 47         139 my $ret;
191 47 100       118 if ($input_by_chars) {
192 7         73 read_loop: while (1) {
193 60 100       200 if (defined $timeout) {
194 30         71 my $start_t= Time::HiRes::time();
195 30 100       987824 if (Crypt::SecretBuffer::Exports::_wait_fh_readable($input_fh, $timeout)) {
196             # deduct from timeout
197 26         67 $timeout -= Time::HiRes::time() - $start_t;
198 26 50       69 $timeout= 0 if $timeout < 0;
199             } else {
200             # use EINTR to signal a timeout was reached, same as if we'd used alarm()
201 4         74 $!= Errno::EINTR();
202 4         14 $ret= undef;
203 4         15 last;
204             }
205             }
206             # Windows Console events can deliver UTF-16 characters which we can transcode to UTF-8
207 56 50 66     666 if ($utf8 && $ttystate && $^O eq 'MSWin32') {
      33        
208 0         0 $ret= eval { $ttystate->_append_console_char($self) };
  0         0  
209 0 0       0 unless (defined $ret) {
210             # Errors about invalid surrogate pairs probably need seen by the user
211             # because their unicode input is not reaching the buffer reliably.
212 0 0       0 if ($@ =~ /surrogate/) {
213 0         0 my $msg= "\nWarning: $@. Your console settings are probably wrong.\n";
214 0 0       0 $prompt_fh? $prompt_fh->print($msg) : warn $msg;
215             }
216 0         0 $ret= undef;
217 0         0 last;
218             }
219 0 0       0 next unless $ret; # false means no complete character yet, try again
220             } else {
221 56 50       6453 $ret= $self->append_read($input_fh, 1)
222             or last; # EOF or system error
223             }
224 56         249 my $end_pos= $self->length - 1;
225             # Handle control characters
226 56 100       583 if ($self->index(qr/[\0-\x1F\x7F]/, $end_pos) == $end_pos) {
    100          
227             # If it is \r or \n, end. If char_count was requested, and we didn't
228             # end by that logic, then we don't have the requested char count, so
229             # return false.
230 3 50       75 if ($self->index(qr/[\r\n]/, $end_pos) == $end_pos) {
    0          
231 3         44 $self->length($end_pos); # remove CR or LF
232 3         9 last;
233             }
234             # handle backspace
235             elsif ($self->index(qr/[\b\x7F]/, $end_pos) == $end_pos) {
236 0         0 $self->length($end_pos); # remove backspace
237 0 0       0 if ($self->length > $start_len) {
238 0         0 $self->length($self->length-1); # remove previous char
239             # print a backspace + space + backspace to erase the final mask character
240 0 0       0 if (length $char_mask) {
241 0 0 0     0 $prompt_fh->print(
242             ("\b" x length $char_mask)
243             .(" " x length $char_mask)
244             .("\b" x length $char_mask))
245             && $prompt_fh->flush
246             or croak "Failed to write $state->{writing_to}: $!";
247             }
248             }
249             }
250             # just ignore any other control char
251             else {
252 0         0 $self->length($end_pos);
253             }
254             }
255             elsif ($utf8) {
256             # count chars, and also find out if there are invalid or incomplete characters
257 28   50     466 my $span= $self->span(pos => ($start_len || 0), encoding => 'UTF-8');
258 28         54 my $count= 0;
259 28         110 while ($span->len) {
260 194         340 my $ch= eval { $span->parse($char_class) };
  194         1168  
261 194 100       802 unless ($ch) {
262 6 50       124 if (!defined $ch) {
263             # partial character? loop again until we get all of it.
264 6         110 $span->encoding(0);
265 6 50       104 my $ch_len= $span->starts_with(qr/[\xC0-\xDF]/)? 2
    100          
266             : $span->starts_with(qr/[\xE0-\xEF]/)? 3
267             : 4;
268 6 50       94 next read_loop if $span->len < $ch_len;
269             # else it's an invalid UTF-8 sequence. Hard to say what the right thing to
270             # do is here, but since passwords need to be an exact match, lets delete
271             # the invalid chars and emit a warning that hopefully the user can see.
272 0         0 my $msg= "\nWarning: discarding invalid UTF-8 sequence. Your console settings are probably wrong.\n";
273 0 0       0 $prompt_fh? $prompt_fh->print($msg) : warn $msg;
274             }
275             # else just not a permitted character. Truncate / ignore it.
276 0         0 $self->length($span->pos);
277 0         0 last;
278             }
279 188 50 33     474 if ($char_max && $count >= $char_max) {
280             # truncate at max chars
281 0         0 $self->length($ch->pos);
282 0         0 last;
283             }
284 188         748 ++$count;
285             }
286             # char added successfully; show progress
287 22 50       60 if (length $char_mask) {
288 0 0 0     0 $prompt_fh->print($char_mask) && $prompt_fh->flush
289             or croak "Failed to write $state->{writing_to}: $!";
290             }
291             # If reached the char_count, return success
292 22 50 33     106 last if $char_count && $count == $char_count;
293             }
294             else {
295 25 50 33     157 if ($char_class && $self->index($char_class, $end_pos) == -1) {
    50 33        
296             # not part of the permitted char class
297 0         0 $self->length($end_pos);
298             }
299             elsif ($char_max && $self->length - $start_len > $char_max) {
300             # refuse to add more characters
301 0         0 $self->length($end_pos);
302             }
303             else {
304             # char added
305 25 50       66 if (length $char_mask) {
306 0 0 0     0 $prompt_fh->print($char_mask) && $prompt_fh->flush
307             or croak "Failed to write $state->{writing_to}: $!";
308             }
309             # If reached the char_count, return success
310 25 50 33     78 last if $char_count && $self->length - $start_len == $char_count;
311             }
312             }
313             }
314             }
315             else {
316 40         1650 $ret= $self->_append_console_line($input_fh);
317 40 50 33     163 if ($char_max && $self->length - $start_len > $char_max) {
318             # truncate the input if char_max requested
319 0         0 $self->length($start_len + $char_max);
320             }
321             }
322             # timeout or system error (including EINTR and EAGAIN)
323 47 100       158 if (!defined $ret) {
324 8         84 return undef;
325             }
326             # If we're responsible for the prompt, also echo the newline to the user so that the caller
327             # doesn't need to figure out what to use for $prompt_fh.
328             $prompt_fh->print("\n") && $prompt_fh->flush
329 39 100 33     144 if defined $state->{prompt};
330             # Not a temporary failuire. Wipe the state.
331 39         201 %$state= ();
332 39 50       286 return !$ret? 0
    100          
333             : $char_count? $self->length - $start_len == $char_count
334             : 1;
335             }
336              
337              
338             sub as_pipe {
339 2     2 1 570982 my $self= shift;
340 2 50       106 pipe(my ($r, $w)) or die "pipe: $!";
341 2         78 $self->write_async($w);
342 2         24 close($w); # XS dups the file handle if it is writing async from a thread
343 2         14 return $r;
344             }
345              
346              
347             sub load_file {
348 1     1 1 1066667 my ($self, $path)= @_;
349 1 50       99 open my $fh, '<', $path or croak "open($path): $!";
350 1         23 my $chunksize= -s $fh;
351 1 50       11 if (!$chunksize) {
352 0         0 $chunksize= sysseek($fh, 0, Fcntl::SEEK_END());
353 0         0 sysseek($fh, 0, Fcntl::SEEK_SET());
354             }
355 1   50     7 $chunksize ||= 64*1024; # if stat doesn't report size and not seekable, just try 64K
356 1         3 while (1) {
357 2         112 my $got= $self->append_sysread($fh, $chunksize);
358 2 50       14 defined $got or croak "sysread($path): $!";
359 2 100       17 last if $got == 0;
360             # should have read the whole thing first try, but file could be changing, so keep going
361             # at 64K intervals until EOF.
362 1         3 $chunksize= 64*1024;
363             }
364 1 50       21 close($fh) or croak "close($path): $!";
365 1         11 return $self;
366             }
367              
368              
369             sub save_file {
370 3     3 1 131049 my ($self, $path, $overwrite)= @_;
371 3         6 my $fh;
372 3         9 my $cur_path= "$path";
373 3 100       13 if (!$overwrite) {
    100          
374 1 50       272 -e $path and croak "File '$path' already exists";
375             # I don't think there's an atomic way to create-without-overwrite in perl, so try this..
376 0 0       0 open $fh, '>>', $path or croak "open($path): $!";
377 0 0       0 croak "File '$path' already exists"
378             if -s $fh > 0;
379             } elsif ($overwrite eq 'rename') {
380 1         9 require File::Temp;
381 1         5 require File::Spec;
382 1         20 my ($vol, $dir, $file)= File::Spec->splitpath($path);
383 1         7 my $dest_dir= File::Spec->catpath($vol, $dir, '');
384 1 50       16 $fh= File::Temp->new(DIR => (length($dest_dir)? $dest_dir : File::Spec->curdir));
385 1         554 $cur_path= "$fh";
386             } else {
387 1 50       92 open $fh, '>', $path or croak "open($path): $!";
388             }
389 2         16 my $wrote= 0;
390 2         56 while ($wrote < $self->length) {
391 2         92 my $w= $self->syswrite($fh, $self->length - $wrote);
392 2 50       9 defined $w or croak "syswrite($cur_path): $!";
393 2         11 $wrote += $w;
394             }
395 2 50       161 close($fh) or croak "close($cur_path): $!";
396 2 100       9 if ($overwrite eq 'rename') {
397 1 50       199 rename($cur_path, $path) or croak "rename($cur_path -> $path): $!";
398 1         7 $fh->unlink_on_destroy(0);
399             }
400 2         30 return $self;
401             }
402              
403              
404             # avoid depending on namespace::clean
405             delete @{Crypt::SecretBuffer::}{qw( carp croak confess )};
406              
407             require Crypt::SecretBuffer::Span;
408             1;
409              
410             __END__