File Coverage

blib/lib/Crypt/SecretBuffer.pm
Criterion Covered Total %
statement 91 134 67.9
branch 33 106 31.1
condition 7 54 12.9
subroutine 16 16 100.0
pod 6 7 85.7
total 153 317 48.2


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