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   88228 use warnings;
  15         28  
  15         461  
5 15     15   121 use strict;
  15         25  
  15         317  
6              
7 15     15   68 use Crypt::PRNG qw(random_bytes);
  15         45  
  15         503  
8 15     15   414 use Devel::GlobalDestruction;
  15         491  
  15         77  
9 15     15   771 use Encode qw(encode decode);
  15         27  
  15         637  
10 15     15   98 use File::KDBX::Constants qw(:random_stream);
  15         29  
  15         1477  
11 15     15   85 use File::KDBX::Error;
  15         26  
  15         714  
12 15     15   99 use File::KDBX::Util qw(erase erase_scoped);
  15         30  
  15         687  
13 15     15   83 use Ref::Util qw(is_arrayref is_coderef is_hashref is_scalarref);
  15         30  
  15         806  
14 15     15   92 use Scalar::Util qw(refaddr);
  15         25  
  15         592  
15 15     15   85 use namespace::clean;
  15         25  
  15         120  
16              
17             our $VERSION = '0.904'; # VERSION
18              
19              
20             sub new {
21 94     94 1 265 my $class = shift;
22 94 100       312 my %args = @_ % 2 == 0 ? @_ : (strings => shift, @_);
23              
24 94 50 66     452 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         201 my $self = bless \%args, $class;
30 94         256 $self->cipher->finish;
31 94         187 $self->{counter} = 0;
32              
33 94         189 my $strings = delete $args{strings};
34 94         201 $self->{items} = [];
35 94         206 $self->{index} = {};
36 94 100       204 $self->add($strings) if $strings;
37              
38 94         778 return $self;
39             }
40              
41 94 50   94   5657 sub DESTROY { local ($., $@, $!, $^E, $?); !in_global_destruction and $_[0]->unlock }
  94         1667  
42              
43              
44             sub clear {
45 180     180 1 252 my $self = shift;
46 180         364 $self->{items} = [];
47 180         532 $self->{index} = {};
48 180         251 $self->{counter} = 0;
49 180         595 return $self;
50             }
51              
52              
53 0     0 1 0 sub lock { shift->add(@_) }
54              
55             sub add {
56 82     82 1 153 my $self = shift;
57 82 100       183 my @strings = map { is_arrayref($_) ? @$_ : $_ } @_;
  82         311  
58              
59 82 50       188 @strings or throw 'Must provide strings to lock';
60              
61 82         171 my $cipher = $self->cipher;
62              
63 82         166 for my $string (@strings) {
64 85         249 my $item = {str => $string, off => $self->{counter}};
65 85 100       180 if (is_scalarref($string)) {
    50          
66 82 50       234 next if !defined $$string;
67 82 50       233 $item->{enc} = 'UTF-8' if utf8::is_utf8($$string);
68 82 50       189 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         229 $item->{val} = $cipher->crypt($string);
75             }
76 82         254 erase $string;
77             }
78             elsif (is_hashref($string)) {
79 3 50       8 next if !defined $string->{value};
80 3 100       8 $item->{enc} = 'UTF-8' if utf8::is_utf8($string->{value});
81 3 100       7 if (my $encoding = $item->{enc}) {
82 1         4 my $encoded = encode($encoding, $string->{value});
83 1         197 $item->{val} = $cipher->crypt(\$encoded);
84 1         3 erase $encoded;
85             }
86             else {
87 2         5 $item->{val} = $cipher->crypt(\$string->{value});
88             }
89 3         16 erase \$string->{value};
90             }
91             else {
92 0         0 throw 'Safe strings must be a hashref or stringref', type => ref $string;
93             }
94 85         120 push @{$self->{items}}, $item;
  85         231  
95 85         369 $self->{index}{refaddr($string)} = $item;
96 85         197 $self->{counter} += length($item->{val});
97             }
98              
99 82         206 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 51 my $self = shift;
107 27 50       66 my $filter = is_coderef($_[0]) ? shift : undef;
108 27 50       58 my @strings = map { is_arrayref($_) ? @$_ : $_ } @_;
  27         87  
109              
110 27 50       62 @strings or throw 'Must provide strings to lock';
111              
112 27         50 for my $string (@strings) {
113 27         94 my $item = {str => $string, off => $self->{counter}};
114 27 50       76 $item->{filter} = $filter if defined $filter;
115 27 50       86 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         53 $item->{val} = $string->{value};
123 27         92 erase \$string->{value};
124             }
125             else {
126 0         0 throw 'Safe strings must be a hashref or stringref', type => ref $string;
127             }
128 27         52 push @{$self->{items}}, $item;
  27         75  
129 27         98 $self->{index}{refaddr($string)} = $item;
130 27         61 $self->{counter} += length($item->{val});
131             }
132              
133 27         66 return $self;
134             }
135              
136              
137             sub unlock {
138 99     99 1 1309 my $self = shift;
139              
140 99         232 my $cipher = $self->cipher;
141 99         325 $cipher->finish;
142 99         164 $self->{counter} = 0;
143              
144 99         131 for my $item (@{$self->{items}}) {
  99         240  
145 31         61 my $string = $item->{str};
146 31         80 my $cleanup = erase_scoped \$item->{val};
147 31         320 my $str_ref;
148 31 100       88 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         97 $string->{value} = $cipher->crypt(\$item->{val});
159 30 100       78 if (my $encoding = $item->{enc}) {
160 1         6 my $decoded = decode($encoding, $string->{value});
161 1         57 erase \$string->{value};
162 1         3 $string->{value} = $decoded;
163             }
164 30         70 $str_ref = \$string->{value};
165             }
166             else {
167 0         0 die 'Unexpected';
168             }
169 31 100       75 if (my $filter = $item->{filter}) {
170 27         70 my $filtered = $filter->($$str_ref);
171 27         1383 erase $str_ref;
172 27         81 $$str_ref = $filtered;
173             }
174             }
175              
176 99         241 return $self->clear;
177             }
178              
179              
180             sub peek {
181 91     91 1 14550 my $self = shift;
182 91         146 my $string = shift;
183              
184 91   50     332 my $item = $self->{index}{refaddr($string)} // return;
185              
186 91         183 my $cipher = $self->cipher->dup(offset => $item->{off});
187              
188 91         246 my $value = $cipher->crypt(\$item->{val});
189 91 50       252 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         315 return $value;
195             }
196              
197              
198             sub cipher {
199 366     366 1 436 my $self = shift;
200 366   66     1033 $self->{cipher} //= do {
201 82         3261 require File::KDBX::Cipher;
202 82         301 File::KDBX::Cipher->new(stream_id => STREAM_ID_CHACHA20, key => random_bytes(64));
203             };
204             }
205              
206             1;
207              
208             __END__