File Coverage

blib/lib/Crypt/SecretBuffer.pm
Criterion Covered Total %
statement 82 112 73.2
branch 25 68 36.7
condition 4 24 16.6
subroutine 16 16 100.0
pod 6 7 85.7
total 133 227 58.5


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.020';
5              
6 23     23   5366581 use strict;
  23         74  
  23         949  
7 23     23   131 use warnings;
  23         50  
  23         1225  
8 23     23   120 use Carp;
  23         37  
  23         1652  
9 23     23   2951 use IO::Handle;
  23         33969  
  23         1125  
10 23     23   147 use Scalar::Util ();
  23         40  
  23         605  
11 23     23   2121 use parent qw( DynaLoader );
  23         1488  
  23         221  
12 23         299 use overload '""' => \&stringify,
13 23     23   2119 'cmp' => \&memcmp;
  23         78  
14              
15 23     23 1 19123 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.020';
22 23     23   3150 use Exporter 'import';
  23         66  
  23         33709  
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   349 splice(@_, 0, 1, 'Crypt::SecretBuffer::Exports');
35 49         274137 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 288693 require Crypt::SecretBuffer::Install::Files;
41 1         4 goto \&Crypt::SecretBuffer::Install::Files::Inline;
42             }
43              
44              
45             sub stringify_mask {
46 4     4 1 483769 my $self= shift;
47 4 100       17 if (@_) {
48 3         76 $self->{stringify_mask}= shift;
49 3         19 return $self;
50             }
51             $self->{stringify_mask}
52 1         9 }
53              
54              
55             sub append_console_line {
56 22     22 1 1183869 my ($self, $handle, %options)= @_;
57             my ($prompt, $prompt_fh, $char_mask, $char_count, $char_max, $char_class)
58 22         92 = delete @options{qw( prompt prompt_fh char_mask char_count char_max char_class )};
59 22 50       80 warn "unknown option: ".join(', ', keys %options)
60             if keys %options;
61 22 50 33     142 if (!$prompt_fh && (defined $prompt || defined $char_mask)) {
      33        
62 0         0 my $fd= fileno($handle);
63 0 0 0     0 if (defined $fd && $fd >= 0) {
    0          
64 0         0 $prompt_fh= IO::Handle->new_from_fd($fd, 'w');
65             } elsif ($handle == \*STDIN) {
66 0         0 $prompt_fh= \*STDOUT;
67             } else {
68 0         0 $prompt_fh= $handle;
69             }
70             }
71             # If the user wants control over the keypresses, need to disable line-editing mode
72 22   33     96 my $input_by_chars= defined $char_mask || defined $char_count || defined $char_class;
73 22         252 my $ttystate= Crypt::SecretBuffer::ConsoleState->maybe_new(
74             handle => $handle,
75             echo => 0,
76             (line_input => 0)x!!$input_by_chars,
77             auto_restore => 1
78             );
79 22 50       40 if (defined $prompt) {
80 0         0 $prompt_fh->print($prompt);
81 0         0 $prompt_fh->flush;
82             }
83 22         87 my $start_len= $self->length;
84 22 50       50 if ($input_by_chars) {
85 0         0 while (1) {
86 0 0       0 $self->append_read($handle, 1)
87             or return undef;
88             # Handle control characters
89 0         0 my $end_pos= $self->length - 1;
90 0 0 0     0 if ($self->index(qr/[\0-\x1F\x7F]/, $end_pos) == $end_pos) {
    0 0        
    0          
91             # If it is \r or \n, end. If char_count was requested, and we didn't
92             # end by that logic, then we don't have the requested char count, so
93             # return false.
94 0 0       0 if ($self->index(qr/[\r\n]/, $end_pos) == $end_pos) {
    0          
95 0         0 $self->length($end_pos); # remove CR or LF
96 0         0 return !$char_count;
97             }
98             # handle backspace
99             elsif ($self->index(qr/[\b\x7F]/, $end_pos) == $end_pos) {
100 0         0 $self->length($end_pos); # remove backspace
101 0 0       0 if ($self->length > $start_len) {
102 0         0 $self->length($self->length-1); # remove previous char
103             # print a backspace + space + backspace to erase the final mask character
104 0 0       0 if (length $char_mask) {
105 0         0 $prompt_fh->print(
106             ("\b" x length $char_mask)
107             .(" " x length $char_mask)
108             .("\b" x length $char_mask));
109 0         0 $prompt_fh->flush;
110             }
111             }
112             }
113             # just ignore any other control char
114             else {
115 0         0 $self->length($end_pos);
116             }
117             }
118             elsif ($char_class && $self->index($char_class, $end_pos) == -1) {
119             # not part of the permitted char class
120 0         0 $self->length($end_pos);
121             }
122             elsif ($char_max && $self->length - $start_len > $char_max) {
123             # refuse to add more characters
124 0         0 $self->length($end_pos);
125             }
126             else {
127             # char added
128 0 0       0 if (length $char_mask) {
129 0         0 $prompt_fh->print($char_mask);
130 0         0 $prompt_fh->flush;
131             }
132             # If reached the char_count, return success
133 0 0 0     0 return 1
134             if $char_count && $self->length - $start_len >= $char_count;
135             }
136             }
137             }
138             else {
139 22         2199 my $ret= $self->_append_console_line($handle);
140 22 50 33     59 if ($char_max && $self->length - $start_len > $char_max) {
141             # truncate the input if char_max requested
142 0         0 $self->length($start_len + $char_max);
143             }
144 22         90 return $ret;
145             }
146             }
147              
148              
149             sub as_pipe {
150 2     2 1 547948 my $self= shift;
151 2 50       104 pipe(my ($r, $w)) or die "pipe: $!";
152 2         144 $self->write_async($w);
153 2         24 close($w); # XS dups the file handle if it is writing async from a thread
154 2         18 return $r;
155             }
156              
157              
158             sub load_file {
159 1     1 1 113962 my ($self, $path)= @_;
160 1 50       37 open my $fh, '<', $path or croak "open($path): $!";
161 1         9 my $blocksize= -s $path;
162 1         2 while (1) {
163 2         21 my $got= $self->append_sysread($fh, $blocksize);
164 2 50       6 defined $got or croak "sysread($path): $!";
165 2 100       7 last if $got == 0;
166             # should have read the whole thing first try, but file could be changing, so keep going
167             # at 16K intervals until EOF.
168 1 50       4 $blocksize= 16*1024 if $blocksize > 16*1024;
169             }
170 1 50       8 close($fh) or croak "close($path): $!";
171 1         7 return $self;
172             }
173              
174              
175             sub save_file {
176 3     3 1 117057 my ($self, $path, $overwrite)= @_;
177 3         7 my $fh;
178 3         7 my $cur_path= "$path";
179 3 100       14 if (!$overwrite) {
    100          
180 1 50       356 -e $path and croak "File '$path' already exists";
181             # I don't think there's an atomic way to create-without-overwrite in perl, so try this..
182 0 0       0 open $fh, '>>', $path or croak "open($path): $!";
183 0 0       0 croak "File '$path' already exists"
184             if -s $fh > 0;
185             } elsif ($overwrite eq 'rename') {
186 1         12 require File::Temp;
187 1         5 require File::Spec;
188 1         25 my ($vol, $dir, $file)= File::Spec->splitpath($path);
189 1         16 $fh= File::Temp->new(DIR => File::Spec->catpath($vol, $dir, ''));
190 1         687 $cur_path= "$fh";
191             } else {
192 1 50       150 open $fh, '>', $path or croak "open($path): $!";
193             }
194 2         17 my $wrote= 0;
195 2         19 while ($wrote < $self->length) {
196 2         97 my $w= $self->syswrite($fh, $self->length - $wrote);
197 2 50       11 defined $w or croak "syswrite($cur_path): $!";
198 2         11 $wrote += $w;
199             }
200 2 50       181 close($fh) or croak "close($cur_path): $!";
201 2 100       44 if ($overwrite eq 'rename') {
202 1 50       527 rename($cur_path, $path) or croak "rename($cur_path -> $path): $!";
203 1         9 $fh->unlink_on_destroy(0);
204             }
205 2         36 return $self;
206             }
207              
208              
209             # avoid depending on namespace::clean
210             delete @{Crypt::SecretBuffer::}{qw( carp croak confess )};
211              
212             require Crypt::SecretBuffer::Span;
213             1;
214              
215             __END__