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   90250 use warnings;
  15         38  
  15         440  
5 15     15   65 use strict;
  15         30  
  15         350  
6              
7 15     15   68 use Crypt::PRNG qw(random_bytes);
  15         43  
  15         569  
8 15     15   432 use Devel::GlobalDestruction;
  15         478  
  15         71  
9 15     15   736 use Encode qw(encode decode);
  15         25  
  15         676  
10 15     15   96 use File::KDBX::Constants qw(:random_stream);
  15         24  
  15         1418  
11 15     15   90 use File::KDBX::Error;
  15         29  
  15         746  
12 15     15   86 use File::KDBX::Util qw(erase erase_scoped);
  15         30  
  15         675  
13 15     15   77 use Ref::Util qw(is_arrayref is_coderef is_hashref is_scalarref);
  15         26  
  15         850  
14 15     15   83 use Scalar::Util qw(refaddr);
  15         33  
  15         536  
15 15     15   105 use namespace::clean;
  15         28  
  15         125  
16              
17             our $VERSION = '0.906'; # VERSION
18              
19              
20             sub new {
21 95     95 1 279 my $class = shift;
22 95 100       290 my %args = @_ % 2 == 0 ? @_ : (strings => shift, @_);
23              
24 95 50 66     371 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 95         183 my $self = bless \%args, $class;
30 95         261 $self->cipher->finish;
31 95         184 $self->{counter} = 0;
32              
33 95         164 my $strings = delete $args{strings};
34 95         181 $self->{items} = [];
35 95         169 $self->{index} = {};
36 95 100       232 $self->add($strings) if $strings;
37              
38 95         778 return $self;
39             }
40              
41 95 50   95   5277 sub DESTROY { local ($., $@, $!, $^E, $?); !in_global_destruction and $_[0]->unlock }
  95         1673  
42              
43              
44             sub clear {
45 182     182 1 232 my $self = shift;
46 182         343 $self->{items} = [];
47 182         488 $self->{index} = {};
48 182         237 $self->{counter} = 0;
49 182         592 return $self;
50             }
51              
52              
53 0     0 1 0 sub lock { shift->add(@_) }
54              
55             sub add {
56 83     83 1 153 my $self = shift;
57 83 100       162 my @strings = map { is_arrayref($_) ? @$_ : $_ } @_;
  83         273  
58              
59 83 50       181 @strings or throw 'Must provide strings to lock';
60              
61 83         150 my $cipher = $self->cipher;
62              
63 83         188 for my $string (@strings) {
64 88         238 my $item = {str => $string, off => $self->{counter}};
65 88 100       188 if (is_scalarref($string)) {
    50          
66 82 50       164 next if !defined $$string;
67 82 50       203 $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         191 $item->{val} = $cipher->crypt($string);
75             }
76 82         251 erase $string;
77             }
78             elsif (is_hashref($string)) {
79 6 50       11 next if !defined $string->{value};
80 6 100       18 $item->{enc} = 'UTF-8' if utf8::is_utf8($string->{value});
81 6 100       13 if (my $encoding = $item->{enc}) {
82 1         5 my $encoded = encode($encoding, $string->{value});
83 1         183 $item->{val} = $cipher->crypt(\$encoded);
84 1         3 erase $encoded;
85             }
86             else {
87 5         13 $item->{val} = $cipher->crypt(\$string->{value});
88             }
89 6         17 erase \$string->{value};
90             }
91             else {
92 0         0 throw 'Safe strings must be a hashref or stringref', type => ref $string;
93             }
94 88         156 push @{$self->{items}}, $item;
  88         204  
95 88         324 $self->{index}{refaddr($string)} = $item;
96 88         180 $self->{counter} += length($item->{val});
97             }
98              
99 83         196 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 48 my $self = shift;
107 27 50       66 my $filter = is_coderef($_[0]) ? shift : undef;
108 27 50       58 my @strings = map { is_arrayref($_) ? @$_ : $_ } @_;
  27         96  
109              
110 27 50       63 @strings or throw 'Must provide strings to lock';
111              
112 27         54 for my $string (@strings) {
113 27         109 my $item = {str => $string, off => $self->{counter}};
114 27 50       90 $item->{filter} = $filter if defined $filter;
115 27 50       77 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       70 next if !defined $string->{value};
122 27         55 $item->{val} = $string->{value};
123 27         75 erase \$string->{value};
124             }
125             else {
126 0         0 throw 'Safe strings must be a hashref or stringref', type => ref $string;
127             }
128 27         38 push @{$self->{items}}, $item;
  27         92  
129 27         125 $self->{index}{refaddr($string)} = $item;
130 27         62 $self->{counter} += length($item->{val});
131             }
132              
133 27         57 return $self;
134             }
135              
136              
137             sub unlock {
138 101     101 1 1334 my $self = shift;
139              
140 101         194 my $cipher = $self->cipher;
141 101         301 $cipher->finish;
142 101         178 $self->{counter} = 0;
143              
144 101         137 for my $item (@{$self->{items}}) {
  101         233  
145 34         56 my $string = $item->{str};
146 34         92 my $cleanup = erase_scoped \$item->{val};
147 34         365 my $str_ref;
148 34 100       212 if (is_scalarref($string)) {
    50          
149 1         3 $$string = $cipher->crypt(\$item->{val});
150 1 50       3 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         2 $str_ref = $string;
156             }
157             elsif (is_hashref($string)) {
158 33         112 $string->{value} = $cipher->crypt(\$item->{val});
159 33 100       93 if (my $encoding = $item->{enc}) {
160 1         4 my $decoded = decode($encoding, $string->{value});
161 1         56 erase \$string->{value};
162 1         2 $string->{value} = $decoded;
163             }
164 33         60 $str_ref = \$string->{value};
165             }
166             else {
167 0         0 die 'Unexpected';
168             }
169 34 100       92 if (my $filter = $item->{filter}) {
170 27         70 my $filtered = $filter->($$str_ref);
171 27         1384 erase $str_ref;
172 27         83 $$str_ref = $filtered;
173             }
174             }
175              
176 101         217 return $self->clear;
177             }
178              
179              
180             sub peek {
181 91     91 1 14064 my $self = shift;
182 91         129 my $string = shift;
183              
184 91   50     328 my $item = $self->{index}{refaddr($string)} // return;
185              
186 91         185 my $cipher = $self->cipher->dup(offset => $item->{off});
187              
188 91         239 my $value = $cipher->crypt(\$item->{val});
189 91 50       263 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         318 return $value;
195             }
196              
197              
198             sub cipher {
199 370     370 1 476 my $self = shift;
200 370   66     1034 $self->{cipher} //= do {
201 83         3506 require File::KDBX::Cipher;
202 83         272 File::KDBX::Cipher->new(stream_id => STREAM_ID_CHACHA20, key => random_bytes(64));
203             };
204             }
205              
206             1;
207              
208             __END__