File Coverage

blib/lib/File/KDBX/Safe.pm
Criterion Covered Total %
statement 129 148 87.1
branch 36 56 64.2
condition 5 8 62.5
subroutine 19 21 90.4
pod 9 9 100.0
total 198 242 81.8


line stmt bran cond sub pod time code
1             package File::KDBX::Safe;
2             # ABSTRACT: Keep strings encrypted while in memory
3              
4 15     15   95496 use warnings;
  15         32  
  15         444  
5 15     15   69 use strict;
  15         27  
  15         348  
6              
7 15     15   64 use Crypt::PRNG qw(random_bytes);
  15         45  
  15         568  
8 15     15   468 use Devel::GlobalDestruction;
  15         540  
  15         93  
9 15     15   792 use Encode qw(encode decode);
  15         41  
  15         631  
10 15     15   106 use File::KDBX::Constants qw(:random_stream);
  15         26  
  15         1490  
11 15     15   81 use File::KDBX::Error;
  15         30  
  15         767  
12 15     15   85 use File::KDBX::Util qw(erase erase_scoped);
  15         25  
  15         709  
13 15     15   82 use Ref::Util qw(is_arrayref is_coderef is_hashref is_scalarref);
  15         36  
  15         777  
14 15     15   88 use Scalar::Util qw(refaddr);
  15         29  
  15         583  
15 15     15   79 use namespace::clean;
  15         22  
  15         130  
16              
17             our $VERSION = '0.905'; # VERSION
18              
19              
20             sub new {
21 94     94 1 277 my $class = shift;
22 94 100       288 my %args = @_ % 2 == 0 ? @_ : (strings => shift, @_);
23              
24 94 50 66     364 if (!$args{cipher} && $args{key}) {
25 0         0 require File::KDBX::Cipher;
26 0         0 $args{cipher} = File::KDBX::Cipher->new(stream_id => STREAM_ID_CHACHA20, key => $args{key});
27             }
28              
29 94         170 my $self = bless \%args, $class;
30 94         233 $self->cipher->finish;
31 94         171 $self->{counter} = 0;
32              
33 94         149 my $strings = delete $args{strings};
34 94         174 $self->{items} = [];
35 94         170 $self->{index} = {};
36 94 100       194 $self->add($strings) if $strings;
37              
38 94         732 return $self;
39             }
40              
41 94 50   94   6353 sub DESTROY { local ($., $@, $!, $^E, $?); !in_global_destruction and $_[0]->unlock }
  94         1648  
42              
43              
44             sub clear {
45 180     180 1 233 my $self = shift;
46 180         341 $self->{items} = [];
47 180         502 $self->{index} = {};
48 180         246 $self->{counter} = 0;
49 180         574 return $self;
50             }
51              
52              
53 0     0 1 0 sub lock { shift->add(@_) }
54              
55             sub add {
56 82     82 1 144 my $self = shift;
57 82 100       164 my @strings = map { is_arrayref($_) ? @$_ : $_ } @_;
  82         275  
58              
59 82 50       185 @strings or throw 'Must provide strings to lock';
60              
61 82         150 my $cipher = $self->cipher;
62              
63 82         166 for my $string (@strings) {
64 85         230 my $item = {str => $string, off => $self->{counter}};
65 85 100       183 if (is_scalarref($string)) {
    50          
66 82 50       181 next if !defined $$string;
67 82 50       210 $item->{enc} = 'UTF-8' if utf8::is_utf8($$string);
68 82 50       172 if (my $encoding = $item->{enc}) {
69 0         0 my $encoded = encode($encoding, $$string);
70 0         0 $item->{val} = $cipher->crypt(\$encoded);
71 0         0 erase $encoded;
72             }
73             else {
74 82         207 $item->{val} = $cipher->crypt($string);
75             }
76 82         246 erase $string;
77             }
78             elsif (is_hashref($string)) {
79 3 50       7 next if !defined $string->{value};
80 3 100       9 $item->{enc} = 'UTF-8' if utf8::is_utf8($string->{value});
81 3 100       19 if (my $encoding = $item->{enc}) {
82 1         6 my $encoded = encode($encoding, $string->{value});
83 1         202 $item->{val} = $cipher->crypt(\$encoded);
84 1         4 erase $encoded;
85             }
86             else {
87 2         7 $item->{val} = $cipher->crypt(\$string->{value});
88             }
89 3         10 erase \$string->{value};
90             }
91             else {
92 0         0 throw 'Safe strings must be a hashref or stringref', type => ref $string;
93             }
94 85         148 push @{$self->{items}}, $item;
  85         199  
95 85         292 $self->{index}{refaddr($string)} = $item;
96 85         185 $self->{counter} += length($item->{val});
97             }
98              
99 82         189 return $self;
100             }
101              
102              
103 0     0 1 0 sub lock_protected { shift->add_protected(@_) }
104              
105             sub add_protected {
106 27     27 1 88 my $self = shift;
107 27 50       81 my $filter = is_coderef($_[0]) ? shift : undef;
108 27 50       65 my @strings = map { is_arrayref($_) ? @$_ : $_ } @_;
  27         115  
109              
110 27 50       97 @strings or throw 'Must provide strings to lock';
111              
112 27         63 for my $string (@strings) {
113 27         92 my $item = {str => $string, off => $self->{counter}};
114 27 50       97 $item->{filter} = $filter if defined $filter;
115 27 50       81 if (is_scalarref($string)) {
    50          
116 0 0       0 next if !defined $$string;
117 0         0 $item->{val} = $$string;
118 0         0 erase $string;
119             }
120             elsif (is_hashref($string)) {
121 27 50       73 next if !defined $string->{value};
122 27         66 $item->{val} = $string->{value};
123 27         82 erase \$string->{value};
124             }
125             else {
126 0         0 throw 'Safe strings must be a hashref or stringref', type => ref $string;
127             }
128 27         39 push @{$self->{items}}, $item;
  27         69  
129 27         128 $self->{index}{refaddr($string)} = $item;
130 27         60 $self->{counter} += length($item->{val});
131             }
132              
133 27         58 return $self;
134             }
135              
136              
137             sub unlock {
138 99     99 1 1248 my $self = shift;
139              
140 99         206 my $cipher = $self->cipher;
141 99         301 $cipher->finish;
142 99         155 $self->{counter} = 0;
143              
144 99         125 for my $item (@{$self->{items}}) {
  99         259  
145 31         61 my $string = $item->{str};
146 31         116 my $cleanup = erase_scoped \$item->{val};
147 31         332 my $str_ref;
148 31 100       95 if (is_scalarref($string)) {
    50          
149 1         4 $$string = $cipher->crypt(\$item->{val});
150 1 50       4 if (my $encoding = $item->{enc}) {
151 0         0 my $decoded = decode($encoding, $string->{value});
152 0         0 erase $string;
153 0         0 $$string = $decoded;
154             }
155 1         3 $str_ref = $string;
156             }
157             elsif (is_hashref($string)) {
158 30         90 $string->{value} = $cipher->crypt(\$item->{val});
159 30 100       84 if (my $encoding = $item->{enc}) {
160 1         6 my $decoded = decode($encoding, $string->{value});
161 1         85 erase \$string->{value};
162 1         3 $string->{value} = $decoded;
163             }
164 30         51 $str_ref = \$string->{value};
165             }
166             else {
167 0         0 die 'Unexpected';
168             }
169 31 100       87 if (my $filter = $item->{filter}) {
170 27         83 my $filtered = $filter->($$str_ref);
171 27         1373 erase $str_ref;
172 27         88 $$str_ref = $filtered;
173             }
174             }
175              
176 99         210 return $self->clear;
177             }
178              
179              
180             sub peek {
181 91     91 1 15045 my $self = shift;
182 91         122 my $string = shift;
183              
184 91   50     349 my $item = $self->{index}{refaddr($string)} // return;
185              
186 91         187 my $cipher = $self->cipher->dup(offset => $item->{off});
187              
188 91         219 my $value = $cipher->crypt(\$item->{val});
189 91 50       243 if (my $encoding = $item->{enc}) {
190 0         0 my $decoded = decode($encoding, $value);
191 0         0 erase $value;
192 0         0 return $decoded;
193             }
194 91         277 return $value;
195             }
196              
197              
198             sub cipher {
199 366     366 1 504 my $self = shift;
200 366   66     1035 $self->{cipher} //= do {
201 82         3167 require File::KDBX::Cipher;
202 82         283 File::KDBX::Cipher->new(stream_id => STREAM_ID_CHACHA20, key => random_bytes(64));
203             };
204             }
205              
206             1;
207              
208             __END__