File Coverage

blib/lib/Crypt/SecretBuffer.pm
Criterion Covered Total %
statement 96 141 68.0
branch 33 106 31.1
condition 8 56 14.2
subroutine 17 17 100.0
pod 6 7 85.7
total 160 327 48.9


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.022';
5              
6 23     23   4440023 use strict;
  23         42  
  23         758  
7 23     23   107 use warnings;
  23         37  
  23         935  
8 23     23   94 use Carp;
  23         35  
  23         1196  
9 23     23   2998 use IO::Handle;
  23         33486  
  23         861  
10 23     23   126 use Scalar::Util ();
  23         38  
  23         364  
11 23     23   95 use Fcntl ();
  23         49  
  23         406  
12 23     23   1943 use parent qw( DynaLoader );
  23         1492  
  23         138  
13 23         217 use overload '""' => \&stringify,
14 23     23   1782 'cmp' => \&memcmp;
  23         57  
15              
16 23     23 1 15668 sub dl_load_flags {0x01} # Share extern symbols with other modules
17             bootstrap Crypt::SecretBuffer;
18              
19              
20             {
21             package Crypt::SecretBuffer::Exports;
22             $Crypt::SecretBuffer::Exports::VERSION = '0.022';
23 23     23   2655 use Exporter 'import';
  23         56  
  23         39952  
24             @Crypt::SecretBuffer::Exports::EXPORT_OK= qw(
25             secret_buffer secret span unmask_secrets_to memcmp
26             NONBLOCK AT_LEAST ISO8859_1 ASCII UTF8 UTF16LE UTF16BE HEX BASE64
27             MATCH_MULTI MATCH_REVERSE MATCH_NEGATE MATCH_ANCHORED MATCH_CONST_TIME
28             );
29             }
30              
31             # Some of the exported functions are not methods, so instead of having them in the object's
32             # namespace I put them in the ::Exports namespace. Importing from Crypt::SecretBuffer is
33             # equivalent to importing from Crypt::SecretBuffer::Exports.
34             sub import {
35 49     49   258 splice(@_, 0, 1, 'Crypt::SecretBuffer::Exports');
36 49         212313 goto \&Crypt::SecretBuffer::Exports::import;
37             }
38              
39             # For "use Inline -with => 'Crypt::SecretBuffer';" but lazy-load the data.
40             sub Inline {
41 1     1 0 146747 require Crypt::SecretBuffer::Install::Files;
42 1         4 goto \&Crypt::SecretBuffer::Install::Files::Inline;
43             }
44              
45              
46             sub stringify_mask {
47 4     4 1 420831 my $self= shift;
48 4 100       15 if (@_) {
49 3         45 $self->{stringify_mask}= shift;
50 3         16 return $self;
51             }
52             $self->{stringify_mask}
53 1         4 }
54              
55              
56             sub append_console_line {
57 22     22 1 1047768 my $self= shift;
58 22         54 my ($input_fh, %options);
59             # First argument can be input_fh, or just straight key/value list.
60 22 50 33     264 if (@_ && ref($_[0]) && (ref $_[0] eq 'GLOB' || ref($_[0])->can('getc'))) {
      33        
      33        
61 22 50       57 croak "Expected even-length list of options" unless @_ & 1;
62 22         47 ($input_fh, %options)= @_;
63             } else {
64 0 0       0 croak "Expected even-length list of options" if @_ & 1;
65 0         0 %options= @_;
66 0         0 $input_fh= delete $options{input_fh};
67             }
68             my ($prompt, $prompt_fh, $char_mask, $char_count, $char_max, $char_class)
69 22         83 = delete @options{qw( prompt prompt_fh char_mask char_count char_max char_class )};
70 22 50       61 warn "unknown option: ".join(', ', keys %options)
71             if keys %options;
72 22         47 my ($reading_from, $writing_to)= ('supplied handle', 'supplied handle');
73 22 50       50 if (!defined $input_fh) {
74             # user is requesting a read from the controlling terminal
75 0 0       0 if ($^O eq 'MSWin32') {
76 0 0       0 open $input_fh, '+<', 'CONIN$' or croak 'open(CONIN$): '.$!;
77 0 0 0     0 open $prompt_fh, '>', 'CONOUT$' or croak 'open(CONOUT$): '.$!
78             unless defined $prompt_fh;
79 0         0 $reading_from= 'CONIN$';
80 0         0 $writing_to= 'CONOUT$';
81             } else {
82 0 0       0 open $input_fh, '+<', '/dev/tty' or croak "open(/dev/tty): $!";
83 0 0       0 $prompt_fh= $input_fh unless defined $prompt_fh;
84 0         0 $reading_from= $writing_to= '/dev/tty';
85             }
86             }
87 22 50 33     133 if (!defined $prompt_fh && (defined $prompt || defined $char_mask)) {
      33        
88             # Determine default prompt_fh
89             # For terminals, if it was STDIN then the underlying descriptors or libc FILE handle
90             # are probably read-only, so open a new writeable handle. Also MSWin32 only has one
91             # console, so do this even if it isn't currently set as STDIN.
92 0         0 my $fd= fileno($input_fh);
93 0 0 0     0 if (-t $input_fh && ((defined $fd && $fd == 0) || \*STDIN == $input_fh || $^O eq 'MSWin32')) {
    0 0        
      0        
94 0 0       0 if ($^O eq 'MSWin32') {
95 0 0       0 open $prompt_fh, '>', 'CONOUT$' or croak 'open(CONOUT$): '.$!;
96 0         0 $writing_to= 'CONOUT$';
97             } else {
98 0 0       0 open $prompt_fh, '+<', '/dev/tty' or croak "open(/dev/tty): $!";
99 0         0 $writing_to= '/dev/tty';
100             }
101             }
102             # For sockets or tty, default to the same file descriptor as input_fh.
103             # If the descriptor is read-only, things will fail, and it's the caller's
104             # job to fix the bug.
105             elsif (-S $input_fh || -t $input_fh) {
106 0         0 $prompt_fh= $input_fh;
107 0         0 $writing_to= 'input handle';
108             }
109             # Suppress prompt unless the handle looks like a TTY or Socket. e.g. input from file
110             # or pipe can't usefully be prompted. It could be that the parent process created a
111             # return pipe on STDOUT and wants to see the prompt there, but it would be too bold to
112             # take a guess at that. The caller can supply prompt_fh => \*STDOUT if they want to.
113             else {
114 0         0 $prompt= $char_mask= undef;
115             }
116             }
117             # If the user wants control over the keypresses, need to disable line-editing mode.
118             # ConsoleState obj with auto_restore restores the console state when it goes out of scope.
119 22   33     140 my $input_by_chars= defined $char_mask || defined $char_count || defined $char_class;
120 22         282 my $ttystate= Crypt::SecretBuffer::ConsoleState->maybe_new(
121             handle => $input_fh,
122             echo => 0,
123             (line_input => 0)x!!$input_by_chars,
124             auto_restore => 1
125             );
126             # Write the initial prompt
127 22 50       63 if (defined $prompt) {
128 0 0 0     0 $prompt_fh->print($prompt) && $prompt_fh->flush
129             or croak "Failed to write $writing_to: $!";
130             }
131 22         74 my $start_len= $self->length;
132 22         40 my $ret;
133 22 50       43 if ($input_by_chars) {
134 0         0 while (1) {
135 0 0       0 $ret= $self->append_read($input_fh, 1)
136             or last;
137             # Handle control characters
138 0         0 my $end_pos= $self->length - 1;
139 0 0 0     0 if ($self->index(qr/[\0-\x1F\x7F]/, $end_pos) == $end_pos) {
    0 0        
    0          
140             # If it is \r or \n, end. If char_count was requested, and we didn't
141             # end by that logic, then we don't have the requested char count, so
142             # return false.
143 0 0       0 if ($self->index(qr/[\r\n]/, $end_pos) == $end_pos) {
    0          
144 0         0 $self->length($end_pos); # remove CR or LF
145 0         0 last;
146             }
147             # handle backspace
148             elsif ($self->index(qr/[\b\x7F]/, $end_pos) == $end_pos) {
149 0         0 $self->length($end_pos); # remove backspace
150 0 0       0 if ($self->length > $start_len) {
151 0         0 $self->length($self->length-1); # remove previous char
152             # print a backspace + space + backspace to erase the final mask character
153 0 0       0 if (length $char_mask) {
154 0 0 0     0 $prompt_fh->print(
155             ("\b" x length $char_mask)
156             .(" " x length $char_mask)
157             .("\b" x length $char_mask))
158             && $prompt_fh->flush
159             or croak "Failed to write $writing_to: $!";
160             }
161             }
162             }
163             # just ignore any other control char
164             else {
165 0         0 $self->length($end_pos);
166             }
167             }
168             elsif ($char_class && $self->index($char_class, $end_pos) == -1) {
169             # not part of the permitted char class
170 0         0 $self->length($end_pos);
171             }
172             elsif ($char_max && $self->length - $start_len > $char_max) {
173             # refuse to add more characters
174 0         0 $self->length($end_pos);
175             }
176             else {
177             # char added
178 0 0       0 if (length $char_mask) {
179 0 0 0     0 $prompt_fh->print($char_mask) && $prompt_fh->flush
180             or croak "Failed to write $writing_to: $!";
181             }
182             # If reached the char_count, return success
183 0 0 0     0 last if $char_count && $self->length - $start_len == $char_count;
184             }
185             }
186             }
187             else {
188 22         925 $ret= $self->_append_console_line($input_fh);
189 22 50 33     95 if ($char_max && $self->length - $start_len > $char_max) {
190             # truncate the input if char_max requested
191 0         0 $self->length($start_len + $char_max);
192             }
193             }
194             # If we're responsible for the prompt, also echo the newline to the user so that the caller
195             # doesn't need to figure out what to use for $prompt_fh.
196 22 50 0     51 $prompt_fh->print("\n") && $prompt_fh->flush
197             if defined $prompt;
198              
199 22 50       134 return !$ret? $ret
    100          
200             : $char_count? $self->length - $start_len == $char_count
201             : 1;
202             }
203              
204              
205             sub as_pipe {
206 2     2 1 334842 my $self= shift;
207 2 50       112 pipe(my ($r, $w)) or die "pipe: $!";
208 2         104 $self->write_async($w);
209 2         22 close($w); # XS dups the file handle if it is writing async from a thread
210 2         14 return $r;
211             }
212              
213              
214             sub load_file {
215 1     1 1 82148 my ($self, $path)= @_;
216 1 50       29 open my $fh, '<', $path or croak "open($path): $!";
217 1         5 my $chunksize= -s $fh;
218 1 50       2 if (!$chunksize) {
219 0         0 $chunksize= sysseek($fh, 0, Fcntl::SEEK_END);
220 0         0 sysseek($fh, 0, Fcntl::SEEK_SET);
221             }
222 1   50     4 $chunksize ||= 64*1024; # if stat doesn't report size and not seekable, just try 64K
223 1         1 while (1) {
224 2         112 my $got= $self->append_sysread($fh, $chunksize);
225 2 50       6 defined $got or croak "sysread($path): $!";
226 2 100       5 last if $got == 0;
227             # should have read the whole thing first try, but file could be changing, so keep going
228             # at 64K intervals until EOF.
229 1         1 $chunksize= 64*1024;
230             }
231 1 50       9 close($fh) or croak "close($path): $!";
232 1         6 return $self;
233             }
234              
235              
236             sub save_file {
237 3     3 1 81232 my ($self, $path, $overwrite)= @_;
238 3         3 my $fh;
239 3         7 my $cur_path= "$path";
240 3 100       9 if (!$overwrite) {
    100          
241 1 50       217 -e $path and croak "File '$path' already exists";
242             # I don't think there's an atomic way to create-without-overwrite in perl, so try this..
243 0 0       0 open $fh, '>>', $path or croak "open($path): $!";
244 0 0       0 croak "File '$path' already exists"
245             if -s $fh > 0;
246             } elsif ($overwrite eq 'rename') {
247 1         6 require File::Temp;
248 1         4 require File::Spec;
249 1         14 my ($vol, $dir, $file)= File::Spec->splitpath($path);
250 1         5 my $dest_dir= File::Spec->catpath($vol, $dir, '');
251 1 50       5 $fh= File::Temp->new(DIR => (length($dest_dir)? $dest_dir : File::Spec->curdir));
252 1         351 $cur_path= "$fh";
253             } else {
254 1 50       64 open $fh, '>', $path or croak "open($path): $!";
255             }
256 2         11 my $wrote= 0;
257 2         10 while ($wrote < $self->length) {
258 2         68 my $w= $self->syswrite($fh, $self->length - $wrote);
259 2 50       6 defined $w or croak "syswrite($cur_path): $!";
260 2         7 $wrote += $w;
261             }
262 2 50       128 close($fh) or croak "close($cur_path): $!";
263 2 100       7 if ($overwrite eq 'rename') {
264 1 50       86 rename($cur_path, $path) or croak "rename($cur_path -> $path): $!";
265 1         4 $fh->unlink_on_destroy(0);
266             }
267 2         20 return $self;
268             }
269              
270              
271             # avoid depending on namespace::clean
272             delete @{Crypt::SecretBuffer::}{qw( carp croak confess )};
273              
274             require Crypt::SecretBuffer::Span;
275             1;
276              
277             __END__