| line | stmt | bran | cond | sub | pod | time | code | 
| 1 |  |  |  |  |  |  | package File::KDBX::Entry; | 
| 2 |  |  |  |  |  |  | # ABSTRACT: A KDBX database entry | 
| 3 |  |  |  |  |  |  |  | 
| 4 | 10 |  |  | 10 |  | 374346 | use warnings; | 
|  | 10 |  |  |  |  | 21 |  | 
|  | 10 |  |  |  |  | 323 |  | 
| 5 | 10 |  |  | 10 |  | 48 | use strict; | 
|  | 10 |  |  |  |  | 16 |  | 
|  | 10 |  |  |  |  | 320 |  | 
| 6 |  |  |  |  |  |  |  | 
| 7 | 10 |  |  | 10 |  | 2774 | use Crypt::Misc 0.049 qw(decode_b64 encode_b32r); | 
|  | 10 |  |  |  |  | 41622 |  | 
|  | 10 |  |  |  |  | 625 |  | 
| 8 | 10 |  |  | 10 |  | 1996 | use Devel::GlobalDestruction; | 
|  | 10 |  |  |  |  | 2504 |  | 
|  | 10 |  |  |  |  | 64 |  | 
| 9 | 10 |  |  | 10 |  | 563 | use Encode qw(encode); | 
|  | 10 |  |  |  |  | 35 |  | 
|  | 10 |  |  |  |  | 417 |  | 
| 10 | 10 |  |  | 10 |  | 104 | use File::KDBX::Constants qw(:history :icon); | 
|  | 10 |  |  |  |  | 20 |  | 
|  | 10 |  |  |  |  | 3832 |  | 
| 11 | 10 |  |  | 10 |  | 71 | use File::KDBX::Error; | 
|  | 10 |  |  |  |  | 23 |  | 
|  | 10 |  |  |  |  | 533 |  | 
| 12 | 10 |  |  | 10 |  | 60 | use File::KDBX::Util qw(:assert :class :coercion :erase :function :uri generate_uuid load_optional); | 
|  | 10 |  |  |  |  | 23 |  | 
|  | 10 |  |  |  |  | 1833 |  | 
| 13 | 10 |  |  | 10 |  | 2524 | use Hash::Util::FieldHash; | 
|  | 10 |  |  |  |  | 4266 |  | 
|  | 10 |  |  |  |  | 414 |  | 
| 14 | 10 |  |  | 10 |  | 55 | use List::Util qw(any first sum0); | 
|  | 10 |  |  |  |  | 16 |  | 
|  | 10 |  |  |  |  | 592 |  | 
| 15 | 10 |  |  | 10 |  | 1230 | use Ref::Util qw(is_coderef is_hashref is_plain_hashref); | 
|  | 10 |  |  |  |  | 21 |  | 
|  | 10 |  |  |  |  | 425 |  | 
| 16 | 10 |  |  | 10 |  | 48 | use Scalar::Util qw(blessed looks_like_number); | 
|  | 10 |  |  |  |  | 17 |  | 
|  | 10 |  |  |  |  | 413 |  | 
| 17 | 10 |  |  | 10 |  | 7816 | use Storable qw(dclone); | 
|  | 10 |  |  |  |  | 28421 |  | 
|  | 10 |  |  |  |  | 582 |  | 
| 18 | 10 |  |  | 10 |  | 66 | use Time::Piece 1.33; | 
|  | 10 |  |  |  |  | 171 |  | 
|  | 10 |  |  |  |  | 72 |  | 
| 19 | 10 |  |  | 10 |  | 738 | use boolean; | 
|  | 10 |  |  |  |  | 21 |  | 
|  | 10 |  |  |  |  | 69 |  | 
| 20 | 10 |  |  | 10 |  | 516 | use namespace::clean; | 
|  | 10 |  |  |  |  | 19 |  | 
|  | 10 |  |  |  |  | 80 |  | 
| 21 |  |  |  |  |  |  |  | 
| 22 |  |  |  |  |  |  | extends 'File::KDBX::Object'; | 
| 23 |  |  |  |  |  |  |  | 
| 24 |  |  |  |  |  |  | our $VERSION = '0.906'; # VERSION | 
| 25 |  |  |  |  |  |  |  | 
| 26 |  |  |  |  |  |  | my $PLACEHOLDER_MAX_DEPTH = 10; | 
| 27 |  |  |  |  |  |  | my %PLACEHOLDERS; | 
| 28 |  |  |  |  |  |  | my %STANDARD_STRINGS = map { $_ => 1 } qw(Title UserName Password URL Notes); | 
| 29 |  |  |  |  |  |  |  | 
| 30 |  |  |  |  |  |  |  | 
| 31 |  |  |  |  |  |  | sub uuid { | 
| 32 | 455 |  |  | 455 | 1 | 573 | my $self = shift; | 
| 33 | 455 | 100 | 100 |  |  | 1439 | if (@_ || !defined $self->{uuid}) { | 
| 34 | 99 | 100 |  |  |  | 308 | my %args = @_ % 2 == 1 ? (uuid => shift, @_) : @_; | 
| 35 | 99 |  |  |  |  | 178 | my $old_uuid = $self->{uuid}; | 
| 36 | 99 |  | 66 |  |  | 378 | my $uuid = $self->{uuid} = delete $args{uuid} // generate_uuid; | 
| 37 | 99 |  |  |  |  | 167 | for my $entry (@{$self->history}) { | 
|  | 99 |  |  |  |  | 255 |  | 
| 38 | 6 |  |  |  |  | 13 | $entry->{uuid} = $uuid; | 
| 39 |  |  |  |  |  |  | } | 
| 40 | 99 | 100 | 66 |  |  | 315 | $self->_signal('uuid.changed', $uuid, $old_uuid) if defined $old_uuid && $self->is_current; | 
| 41 |  |  |  |  |  |  | } | 
| 42 | 455 |  |  |  |  | 1113 | $self->{uuid}; | 
| 43 | 183 | 50 |  | 183 | 1 | 641 | } | 
| 44 | 183 | 100 |  | 99 | 1 | 598 |  | 
|  | 99 | 50 |  |  |  | 319 |  | 
| 45 | 183 | 100 | 66 | 85 | 1 | 1741 | # has uuid                    => sub { generate_uuid(printable => 1) }; | 
|  | 99 | 50 |  |  |  | 240 |  | 
|  | 85 |  |  |  |  | 260 |  | 
| 46 | 99 | 50 | 100 | 85 | 1 | 512 | has icon_id                 => ICON_PASSWORD,   coerce => \&to_icon_constant; | 
|  | 85 | 50 |  |  |  | 186 |  | 
|  | 85 |  |  |  |  | 274 |  | 
| 47 | 85 | 50 | 33 | 85 | 1 | 564 | has custom_icon_uuid        => undef,           coerce => \&to_uuid; | 
|  | 85 | 50 |  |  |  | 180 |  | 
|  | 85 |  |  |  |  | 277 |  | 
| 48 | 85 | 50 | 33 | 88 | 1 | 438 | has foreground_color        => '',              coerce => \&to_string; | 
|  | 85 | 50 |  |  |  | 236 |  | 
|  | 88 |  |  |  |  | 251 |  | 
| 49 | 85 | 50 | 33 | 517 | 0 | 444 | has background_color        => '',              coerce => \&to_string; | 
|  | 88 | 50 |  |  |  | 191 |  | 
|  | 517 |  |  |  |  | 1284 |  | 
| 50 | 88 | 50 | 66 | 114 | 1 | 430 | has override_url            => '',              coerce => \&to_string; | 
|  | 517 | 50 |  |  |  | 971 |  | 
|  | 114 |  |  |  |  | 347 |  | 
| 51 | 517 | 100 | 100 | 139 | 1 | 2308 | has tags                    => '',              coerce => \&to_string; | 
|  | 114 | 50 |  |  |  | 247 |  | 
|  | 139 |  |  |  |  | 488 |  | 
| 52 | 114 | 100 | 100 | 138 | 1 | 602 | has auto_type               => {}; | 
|  | 139 | 50 |  |  |  | 268 |  | 
|  | 138 |  |  |  |  | 899 |  | 
| 53 | 139 | 50 | 66 | 136 | 1 | 592 | has previous_parent_group   => undef,           coerce => \&to_uuid; | 
|  | 138 | 50 |  |  |  | 287 |  | 
|  | 136 |  |  |  |  | 1520 |  | 
| 54 | 138 | 50 | 50 | 728 | 0 | 484 | has quality_check           => true,            coerce => \&to_bool; | 
|  | 136 | 50 |  |  |  | 309 |  | 
|  | 728 |  |  |  |  | 1781 |  | 
| 55 | 136 | 50 | 100 |  |  | 690 | has strings                 => {}; | 
|  | 728 |  |  |  |  | 1162 |  | 
| 56 | 728 |  | 100 |  |  | 3218 | has binaries                => {}; | 
| 57 |  |  |  |  |  |  | has times                   => {}; | 
| 58 | 101 | 50 |  | 101 | 1 | 369 | # has custom_data             => {}; | 
| 59 | 101 | 100 |  | 88 | 1 | 269 | # has history                 => []; | 
|  | 88 | 50 |  |  |  | 9188 |  | 
| 60 | 101 | 100 | 100 | 93 | 1 | 198 |  | 
|  | 88 | 50 |  |  |  | 215 |  | 
|  | 93 |  |  |  |  | 5217 |  | 
| 61 | 88 | 100 | 100 | 85 | 1 | 275 | has last_modification_time  => sub { gmtime }, store => 'times', coerce => \&to_time; | 
|  | 93 | 50 |  |  |  | 279 |  | 
|  | 85 |  |  |  |  | 5514 |  | 
| 62 | 93 | 50 | 100 | 85 | 1 | 215 | has creation_time           => sub { gmtime }, store => 'times', coerce => \&to_time; | 
|  | 85 | 50 |  |  |  | 233 |  | 
|  | 85 |  |  |  |  | 5011 |  | 
| 63 | 85 | 50 | 50 | 85 | 1 | 168 | has last_access_time        => sub { gmtime }, store => 'times', coerce => \&to_time; | 
|  | 85 | 50 |  |  |  | 245 |  | 
|  | 85 |  |  |  |  | 660 |  | 
| 64 | 85 | 50 | 33 | 86 | 1 | 183 | has expiry_time             => sub { gmtime }, store => 'times', coerce => \&to_time; | 
|  | 85 | 50 |  |  |  | 231 |  | 
|  | 86 |  |  |  |  | 277 |  | 
| 65 | 85 | 100 | 33 |  |  | 169 | has expires                 => false,          store => 'times', coerce => \&to_bool; | 
|  | 86 |  |  |  |  | 197 |  | 
| 66 | 86 |  | 100 |  |  | 166 | has usage_count             => 0,              store => 'times', coerce => \&to_number; | 
| 67 | 85 | 50 |  | 85 | 1 | 5202 | has location_changed        => sub { gmtime }, store => 'times', coerce => \&to_time; | 
| 68 | 85 | 50 |  |  |  | 224 |  | 
| 69 | 85 | 50 | 33 | 85 | 1 | 219 | # has 'auto_type.auto_type_enabled'                   => true, coerce => \&to_bool; | 
|  | 85 |  |  |  |  | 281 |  | 
| 70 | 85 | 50 |  |  |  | 215 | has 'auto_type_obfuscation' => 0, path => 'auto_type.data_transfer_obfuscation', | 
| 71 | 85 | 50 | 33 | 88 | 1 | 193 | coerce => \&to_number; | 
|  | 88 |  |  |  |  | 317 |  | 
| 72 | 88 | 50 |  |  |  | 191 | has 'auto_type_default_sequence'          => '{USERNAME}{TAB}{PASSWORD}{ENTER}', | 
| 73 | 88 |  | 100 |  |  | 184 | path => 'auto_type.default_sequence', coerce => \&to_string; | 
| 74 |  |  |  |  |  |  | has 'auto_type_associations'              => [], path => 'auto_type.associations'; | 
| 75 |  |  |  |  |  |  |  | 
| 76 |  |  |  |  |  |  | my %ATTRS_STRINGS = ( | 
| 77 |  |  |  |  |  |  | title                   => 'Title', | 
| 78 |  |  |  |  |  |  | username                => 'UserName', | 
| 79 |  |  |  |  |  |  | password                => 'Password', | 
| 80 |  |  |  |  |  |  | url                     => 'URL', | 
| 81 |  |  |  |  |  |  | notes                   => 'Notes', | 
| 82 |  |  |  |  |  |  | ); | 
| 83 |  |  |  |  |  |  | while (my ($attr, $string_key) = each %ATTRS_STRINGS) { | 
| 84 | 10 |  |  | 10 |  | 16190 | no strict 'refs'; ## no critic (ProhibitNoStrict) | 
|  | 10 |  |  |  |  | 25 |  | 
|  | 10 |  |  |  |  | 42770 |  | 
| 85 | 492 |  |  | 492 |  | 7634 | *{$attr} = sub { shift->string_value($string_key, @_) }; | 
| 86 | 323 |  |  | 323 |  | 1571 | *{"expand_${attr}"} = sub { shift->expand_string_value($string_key, @_) }; | 
| 87 |  |  |  |  |  |  | } | 
| 88 |  |  |  |  |  |  |  | 
| 89 |  |  |  |  |  |  | my @ATTRS = qw(uuid custom_data history auto_type_enabled); | 
| 90 |  |  |  |  |  |  | sub _set_nonlazy_attributes { | 
| 91 | 85 |  |  | 85 |  | 121 | my $self = shift; | 
| 92 | 85 |  |  |  |  | 481 | $self->$_ for @ATTRS, keys %ATTRS_STRINGS, list_attributes(ref $self); | 
| 93 |  |  |  |  |  |  | } | 
| 94 |  |  |  |  |  |  |  | 
| 95 |  |  |  |  |  |  | sub init { | 
| 96 | 105 |  |  | 105 | 1 | 164 | my $self = shift; | 
| 97 | 105 |  |  |  |  | 257 | my %args = @_; | 
| 98 |  |  |  |  |  |  |  | 
| 99 | 105 |  |  |  |  | 375 | while (my ($key, $val) = each %args) { | 
| 100 | 160 | 100 |  |  |  | 574 | if (my $method = $self->can($key)) { | 
| 101 | 158 |  |  |  |  | 332 | $self->$method($val); | 
| 102 |  |  |  |  |  |  | } | 
| 103 |  |  |  |  |  |  | else { | 
| 104 | 2 |  |  |  |  | 4 | $self->string($key => $val); | 
| 105 |  |  |  |  |  |  | } | 
| 106 |  |  |  |  |  |  | } | 
| 107 |  |  |  |  |  |  |  | 
| 108 | 105 |  |  |  |  | 252 | return $self; | 
| 109 |  |  |  |  |  |  | } | 
| 110 |  |  |  |  |  |  |  | 
| 111 |  |  |  |  |  |  | ############################################################################## | 
| 112 |  |  |  |  |  |  |  | 
| 113 |  |  |  |  |  |  |  | 
| 114 |  |  |  |  |  |  | sub string { | 
| 115 | 1279 |  |  | 1279 | 1 | 1648 | my $self = shift; | 
| 116 | 1279 | 100 |  |  |  | 3977 | my %args = @_     == 2 ? (key => shift, value => shift) | 
|  |  | 100 |  |  |  |  |  | 
| 117 |  |  |  |  |  |  | : @_ % 2 == 1 ? (key => shift, @_) : @_; | 
| 118 |  |  |  |  |  |  |  | 
| 119 | 1279 | 50 | 66 |  |  | 2402 | if (!defined $args{key} && !defined $args{value}) { | 
| 120 | 1 |  |  |  |  | 4 | my %standard = (value => 1, protect => 1); | 
| 121 | 1 |  |  |  |  | 3 | my @other_keys = grep { !$standard{$_} } keys %args; | 
|  | 2 |  |  |  |  | 5 |  | 
| 122 | 1 | 50 |  |  |  | 3 | if (@other_keys == 1) { | 
| 123 | 1 |  |  |  |  | 3 | my $key = $args{key} = $other_keys[0]; | 
| 124 | 1 |  |  |  |  | 2 | $args{value} = delete $args{$key}; | 
| 125 |  |  |  |  |  |  | } | 
| 126 |  |  |  |  |  |  | } | 
| 127 |  |  |  |  |  |  |  | 
| 128 | 1279 | 50 |  |  |  | 2467 | my $key = delete $args{key} or throw 'Must provide a string key to access'; | 
| 129 |  |  |  |  |  |  |  | 
| 130 | 1279 | 50 |  |  |  | 3677 | return $self->{strings}{$key} = $args{value} if is_plain_hashref($args{value}); | 
| 131 |  |  |  |  |  |  |  | 
| 132 |  |  |  |  |  |  | # Auto-vivify the standard strings. | 
| 133 | 1279 | 100 | 100 |  |  | 4153 | if (!exists $self->{strings}{$key} && $STANDARD_STRINGS{$key}) { | 
| 134 | 426 |  | 100 |  |  | 1286 | $args{value} //= ''; | 
| 135 | 426 | 100 | 33 |  |  | 719 | $args{protect} //= true if $self->_protect($key); | 
| 136 |  |  |  |  |  |  | } | 
| 137 |  |  |  |  |  |  |  | 
| 138 | 1279 |  |  |  |  | 4367 | while (my ($field, $value) = each %args) { | 
| 139 | 587 |  |  |  |  | 2110 | $self->{strings}{$key}{$field} = $value; | 
| 140 |  |  |  |  |  |  | } | 
| 141 |  |  |  |  |  |  |  | 
| 142 | 1279 |  |  |  |  | 3530 | return $self->{strings}{$key}; | 
| 143 |  |  |  |  |  |  | } | 
| 144 |  |  |  |  |  |  |  | 
| 145 |  |  |  |  |  |  | ### Get whether or not a standard string is configured to be protected | 
| 146 |  |  |  |  |  |  | sub _protect { | 
| 147 | 426 |  |  | 426 |  | 525 | my $self = shift; | 
| 148 | 426 |  |  |  |  | 558 | my $key  = shift; | 
| 149 | 426 | 50 |  |  |  | 772 | return false if !$STANDARD_STRINGS{$key}; | 
| 150 | 426 | 100 |  |  |  | 567 | if (my $kdbx = eval { $self->kdbx }) { | 
|  | 426 |  |  |  |  | 1543 |  | 
| 151 | 111 |  |  |  |  | 225 | my $protect = $kdbx->memory_protection($key); | 
| 152 | 111 | 50 |  |  |  | 380 | return $protect if defined $protect; | 
| 153 |  |  |  |  |  |  | } | 
| 154 | 315 |  |  |  |  | 1210 | return $key eq 'Password'; | 
| 155 |  |  |  |  |  |  | } | 
| 156 |  |  |  |  |  |  |  | 
| 157 |  |  |  |  |  |  |  | 
| 158 |  |  |  |  |  |  | sub string_value { | 
| 159 | 890 |  |  | 890 | 1 | 22653 | my $self = shift; | 
| 160 | 890 |  | 100 |  |  | 1486 | my $string = $self->string(@_) // return undef; | 
| 161 | 614 |  |  |  |  | 2188 | return $string->{value}; | 
| 162 |  |  |  |  |  |  | } | 
| 163 |  |  |  |  |  |  |  | 
| 164 |  |  |  |  |  |  |  | 
| 165 |  |  |  |  |  |  | sub _expand_placeholder { | 
| 166 | 43 |  |  | 43 |  | 73 | my $self = shift; | 
| 167 | 43 |  |  |  |  | 57 | my $placeholder = shift; | 
| 168 | 43 |  |  |  |  | 67 | my $arg = shift; | 
| 169 |  |  |  |  |  |  |  | 
| 170 | 43 |  |  |  |  | 149 | require File::KDBX; | 
| 171 |  |  |  |  |  |  |  | 
| 172 | 43 |  |  |  |  | 62 | my $placeholder_key = $placeholder; | 
| 173 | 43 | 100 |  |  |  | 77 | if (defined $arg) { | 
| 174 | 12 | 50 |  |  |  | 45 | $placeholder_key = $File::KDBX::PLACEHOLDERS{"${placeholder}:${arg}"} ? "${placeholder}:${arg}" | 
| 175 |  |  |  |  |  |  | : "${placeholder}:"; | 
| 176 |  |  |  |  |  |  | } | 
| 177 | 43 | 100 |  |  |  | 96 | return if !defined $File::KDBX::PLACEHOLDERS{$placeholder_key}; | 
| 178 |  |  |  |  |  |  |  | 
| 179 | 42 |  |  |  |  | 109 | my $local_key = join('/', Hash::Util::FieldHash::id($self), $placeholder_key); | 
| 180 | 42 |  | 66 |  |  | 99 | local $PLACEHOLDERS{$local_key} = my $handler = $PLACEHOLDERS{$local_key} // do { | 
| 181 | 23 | 50 |  |  |  | 47 | my $handler = $File::KDBX::PLACEHOLDERS{$placeholder_key} or next; | 
| 182 |  |  |  |  |  |  | memoize recurse_limit($handler, $PLACEHOLDER_MAX_DEPTH, sub { | 
| 183 | 1 |  |  | 1 |  | 8 | alert "Detected deep recursion while expanding $placeholder placeholder", | 
| 184 |  |  |  |  |  |  | placeholder => $placeholder; | 
| 185 | 1 |  |  |  |  | 19 | return; # undef | 
| 186 | 23 |  |  |  |  | 90 | }); | 
| 187 |  |  |  |  |  |  | }; | 
| 188 |  |  |  |  |  |  |  | 
| 189 | 42 |  |  |  |  | 129 | return $handler->($self, $arg, $placeholder); | 
| 190 |  |  |  |  |  |  | } | 
| 191 |  |  |  |  |  |  |  | 
| 192 |  |  |  |  |  |  | sub _expand_string { | 
| 193 | 323 |  |  | 323 |  | 430 | my $self    = shift; | 
| 194 | 323 |  |  |  |  | 436 | my $str     = shift; | 
| 195 |  |  |  |  |  |  |  | 
| 196 | 323 |  |  |  |  | 1200 | my $expand = memoize $self->can('_expand_placeholder'), $self; | 
| 197 |  |  |  |  |  |  |  | 
| 198 |  |  |  |  |  |  | # placeholders (including field references): | 
| 199 | 323 |  | 66 |  |  | 868 | $str =~ s!\{([^:\}]+)(?::([^\}]*))?\}!$expand->(uc($1), $2, @_) // $&!egi; | 
|  | 44 |  |  |  |  | 184 |  | 
| 200 |  |  |  |  |  |  |  | 
| 201 |  |  |  |  |  |  | # environment variables (alt syntax): | 
| 202 | 323 |  |  |  |  | 2040 | my $vars = join('|', map { quotemeta($_) } keys %ENV); | 
|  | 10984 |  |  |  |  | 15029 |  | 
| 203 | 323 |  | 33 |  |  | 3591 | $str =~ s!\%($vars)\%!$expand->(ENV => $1, @_) // $&!eg; | 
|  | 2 |  |  |  |  | 8 |  | 
| 204 |  |  |  |  |  |  |  | 
| 205 | 323 |  |  |  |  | 2209 | return $str; | 
| 206 |  |  |  |  |  |  | } | 
| 207 |  |  |  |  |  |  |  | 
| 208 |  |  |  |  |  |  | sub expand_string_value { | 
| 209 | 323 |  |  | 323 | 1 | 419 | my $self = shift; | 
| 210 | 323 |  | 50 |  |  | 657 | my $str  = $self->string_peek(@_) // return undef; | 
| 211 | 323 |  |  |  |  | 905 | my $cleanup = erase_scoped $str; | 
| 212 | 323 |  |  |  |  | 3762 | return $self->_expand_string($str); | 
| 213 |  |  |  |  |  |  | } | 
| 214 |  |  |  |  |  |  |  | 
| 215 |  |  |  |  |  |  |  | 
| 216 |  |  |  |  |  |  | sub other_strings { | 
| 217 | 5 |  |  | 5 | 1 | 9 | my $self    = shift; | 
| 218 | 5 |  | 50 |  |  | 14 | my $delim   = shift // "\n"; | 
| 219 |  |  |  |  |  |  |  | 
| 220 | 5 |  |  |  |  | 7 | my @strings = map { $self->string_value($_) } grep { !$STANDARD_STRINGS{$_} } sort keys %{$self->strings}; | 
|  | 3 |  |  |  |  | 6 |  | 
|  | 28 |  |  |  |  | 43 |  | 
|  | 5 |  |  |  |  | 9 |  | 
| 221 | 5 |  |  |  |  | 17 | return join($delim, @strings); | 
| 222 |  |  |  |  |  |  | } | 
| 223 |  |  |  |  |  |  |  | 
| 224 |  |  |  |  |  |  |  | 
| 225 |  |  |  |  |  |  | sub string_peek { | 
| 226 | 325 |  |  | 325 | 1 | 461 | my $self = shift; | 
| 227 | 325 |  |  |  |  | 653 | my $string = $self->string(@_); | 
| 228 | 325 | 100 |  |  |  | 1032 | return defined $string->{value} ? $string->{value} : $self->kdbx->peek($string); | 
| 229 |  |  |  |  |  |  | } | 
| 230 |  |  |  |  |  |  |  | 
| 231 |  |  |  |  |  |  | ############################################################################## | 
| 232 |  |  |  |  |  |  |  | 
| 233 |  |  |  |  |  |  |  | 
| 234 |  |  |  |  |  |  | sub add_auto_type_association { | 
| 235 | 2 |  |  | 2 | 1 | 11 | my $self        = shift; | 
| 236 | 2 |  |  |  |  | 2 | my $association = shift; | 
| 237 | 2 |  |  |  |  | 4 | push @{$self->auto_type_associations}, $association; | 
|  | 2 |  |  |  |  | 3 |  | 
| 238 |  |  |  |  |  |  | } | 
| 239 |  |  |  |  |  |  |  | 
| 240 |  |  |  |  |  |  |  | 
| 241 |  |  |  |  |  |  | sub expand_keystroke_sequence { | 
| 242 | 0 |  |  | 0 | 1 | 0 | my $self = shift; | 
| 243 | 0 |  |  |  |  | 0 | my $association = shift; | 
| 244 |  |  |  |  |  |  |  | 
| 245 | 0 |  |  |  |  | 0 | my $keys; | 
| 246 | 0 | 0 |  |  |  | 0 | if ($association) { | 
| 247 |  |  |  |  |  |  | $keys = is_hashref($association) && exists $association->{keystroke_sequence} ? | 
| 248 | 0 | 0 | 0 |  |  | 0 | $association->{keystroke_sequence} : defined $association ? $association : ''; | 
|  |  | 0 |  |  |  |  |  | 
| 249 |  |  |  |  |  |  | } | 
| 250 |  |  |  |  |  |  |  | 
| 251 | 0 | 0 |  |  |  | 0 | $keys = $self->auto_type_default_sequence if !$keys; | 
| 252 |  |  |  |  |  |  | # TODO - Fall back to getting default sequence from parent group, which probably means we shouldn't be | 
| 253 |  |  |  |  |  |  | # setting a default value in the entry.. | 
| 254 |  |  |  |  |  |  |  | 
| 255 | 0 |  |  |  |  | 0 | return $self->_expand_string($keys); | 
| 256 |  |  |  |  |  |  | } | 
| 257 |  |  |  |  |  |  |  | 
| 258 |  |  |  |  |  |  | ############################################################################## | 
| 259 |  |  |  |  |  |  |  | 
| 260 |  |  |  |  |  |  |  | 
| 261 |  |  |  |  |  |  | sub binary { | 
| 262 | 8 |  |  | 8 | 1 | 20 | my $self = shift; | 
| 263 | 8 | 100 |  |  |  | 43 | my %args = @_     == 2 ? (key => shift, value => shift) | 
|  |  | 100 |  |  |  |  |  | 
| 264 |  |  |  |  |  |  | : @_ % 2 == 1 ? (key => shift, @_) : @_; | 
| 265 |  |  |  |  |  |  |  | 
| 266 | 8 | 50 | 66 |  |  | 27 | if (!defined $args{key} && !defined $args{value}) { | 
| 267 | 1 |  |  |  |  | 4 | my %standard = (value => 1, protect => 1); | 
| 268 | 1 |  |  |  |  | 3 | my @other_keys = grep { !$standard{$_} } keys %args; | 
|  | 2 |  |  |  |  | 5 |  | 
| 269 | 1 | 50 |  |  |  | 3 | if (@other_keys == 1) { | 
| 270 | 1 |  |  |  |  | 3 | my $key = $args{key} = $other_keys[0]; | 
| 271 | 1 |  |  |  |  | 2 | $args{value} = delete $args{$key}; | 
| 272 |  |  |  |  |  |  | } | 
| 273 |  |  |  |  |  |  | } | 
| 274 |  |  |  |  |  |  |  | 
| 275 | 8 | 50 |  |  |  | 21 | my $key = delete $args{key} or throw 'Must provide a binary key to access'; | 
| 276 |  |  |  |  |  |  |  | 
| 277 | 8 | 50 |  |  |  | 29 | return $self->{binaries}{$key} = $args{value} if is_plain_hashref($args{value}); | 
| 278 |  |  |  |  |  |  |  | 
| 279 | 8 |  | 0 | 0 |  | 44 | assert { !defined $args{value} || !utf8::is_utf8($args{value}) }; | 
|  | 0 |  |  |  |  | 0 |  | 
| 280 | 8 |  |  |  |  | 44 | while (my ($field, $value) = each %args) { | 
| 281 | 3 |  |  |  |  | 9 | $self->{binaries}{$key}{$field} = $value; | 
| 282 |  |  |  |  |  |  | } | 
| 283 | 8 |  |  |  |  | 29 | return $self->{binaries}{$key}; | 
| 284 |  |  |  |  |  |  | } | 
| 285 |  |  |  |  |  |  |  | 
| 286 |  |  |  |  |  |  |  | 
| 287 |  |  |  |  |  |  | sub binary_value { | 
| 288 | 4 |  |  | 4 | 1 | 8 | my $self = shift; | 
| 289 | 4 |  | 50 |  |  | 10 | my $binary = $self->binary(@_) // return undef; | 
| 290 | 4 |  |  |  |  | 16 | return $binary->{value}; | 
| 291 |  |  |  |  |  |  | } | 
| 292 |  |  |  |  |  |  |  | 
| 293 |  |  |  |  |  |  | ############################################################################## | 
| 294 |  |  |  |  |  |  |  | 
| 295 |  |  |  |  |  |  |  | 
| 296 |  |  |  |  |  |  | sub hmac_otp { | 
| 297 | 27 |  |  | 27 | 1 | 57 | my $self = shift; | 
| 298 | 27 |  |  |  |  | 173 | load_optional('Pass::OTP'); | 
| 299 |  |  |  |  |  |  |  | 
| 300 | 27 |  |  |  |  | 136 | my %params = ($self->_hotp_params, @_); | 
| 301 | 27 | 50 | 33 |  |  | 206 | return if !defined $params{type} || !defined $params{secret}; | 
| 302 |  |  |  |  |  |  |  | 
| 303 | 27 | 100 |  |  |  | 150 | $params{secret} = encode_b32r($params{secret}) if !$params{base32}; | 
| 304 | 27 |  |  |  |  | 70 | $params{base32} = 1; | 
| 305 |  |  |  |  |  |  |  | 
| 306 | 27 |  |  |  |  | 44 | my $otp = eval { Pass::OTP::otp(%params, @_) }; | 
|  | 27 |  |  |  |  | 171 |  | 
| 307 | 27 | 50 |  |  |  | 18022 | if (my $err = $@) { | 
| 308 | 0 |  |  |  |  | 0 | throw 'Unable to generate HOTP', error => $err; | 
| 309 |  |  |  |  |  |  | } | 
| 310 |  |  |  |  |  |  |  | 
| 311 | 27 |  |  |  |  | 124 | $self->_hotp_increment_counter($params{counter}); | 
| 312 |  |  |  |  |  |  |  | 
| 313 | 27 |  |  |  |  | 239 | return $otp; | 
| 314 |  |  |  |  |  |  | } | 
| 315 |  |  |  |  |  |  |  | 
| 316 |  |  |  |  |  |  |  | 
| 317 |  |  |  |  |  |  | sub time_otp { | 
| 318 | 10 |  |  | 10 | 1 | 24 | my $self = shift; | 
| 319 | 10 |  |  |  |  | 71 | load_optional('Pass::OTP'); | 
| 320 |  |  |  |  |  |  |  | 
| 321 | 10 |  |  |  |  | 50 | my %params = ($self->_totp_params, @_); | 
| 322 | 10 | 50 | 33 |  |  | 75 | return if !defined $params{type} || !defined $params{secret}; | 
| 323 |  |  |  |  |  |  |  | 
| 324 | 10 | 50 |  |  |  | 29 | $params{secret} = encode_b32r($params{secret}) if !$params{base32}; | 
| 325 | 10 |  |  |  |  | 22 | $params{base32} = 1; | 
| 326 |  |  |  |  |  |  |  | 
| 327 | 10 |  |  |  |  | 18 | my $otp = eval { Pass::OTP::otp(%params, @_) }; | 
|  | 10 |  |  |  |  | 76 |  | 
| 328 | 10 | 50 |  |  |  | 8655 | if (my $err = $@) { | 
| 329 | 0 |  |  |  |  | 0 | throw 'Unable to generate TOTP', error => $err; | 
| 330 |  |  |  |  |  |  | } | 
| 331 |  |  |  |  |  |  |  | 
| 332 | 10 |  |  |  |  | 108 | return $otp; | 
| 333 |  |  |  |  |  |  | } | 
| 334 |  |  |  |  |  |  |  | 
| 335 |  |  |  |  |  |  |  | 
| 336 | 9 |  |  | 9 | 1 | 86 | sub hmac_otp_uri { $_[0]->_otp_uri($_[0]->_hotp_params) } | 
| 337 | 11 |  |  | 11 | 1 | 151 | sub time_otp_uri { $_[0]->_otp_uri($_[0]->_totp_params) } | 
| 338 |  |  |  |  |  |  |  | 
| 339 |  |  |  |  |  |  | sub _otp_uri { | 
| 340 | 20 |  |  | 20 |  | 38 | my $self = shift; | 
| 341 | 20 |  |  |  |  | 88 | my %params = @_; | 
| 342 |  |  |  |  |  |  |  | 
| 343 | 20 | 50 |  |  |  | 81 | return if 4 != grep { defined } @params{qw(type secret issuer account)}; | 
|  | 80 |  |  |  |  | 159 |  | 
| 344 | 20 | 50 |  |  |  | 140 | return if $params{type} !~ /^[ht]otp$/i; | 
| 345 |  |  |  |  |  |  |  | 
| 346 | 20 |  |  |  |  | 50 | my $label = delete $params{label}; | 
| 347 | 20 |  |  |  |  | 141 | $params{$_} = uri_escape_utf8($params{$_}) for keys %params; | 
| 348 |  |  |  |  |  |  |  | 
| 349 | 20 |  |  |  |  | 63 | my $type    = lc($params{type}); | 
| 350 | 20 |  |  |  |  | 31 | my $issuer  = $params{issuer}; | 
| 351 | 20 |  |  |  |  | 37 | my $account = $params{account}; | 
| 352 |  |  |  |  |  |  |  | 
| 353 | 20 |  | 66 |  |  | 86 | $label //= "$issuer:$account"; | 
| 354 |  |  |  |  |  |  |  | 
| 355 | 20 |  |  |  |  | 54 | my $secret = $params{secret}; | 
| 356 | 20 | 100 |  |  |  | 91 | $secret = uc(encode_b32r($secret)) if !$params{base32}; | 
| 357 |  |  |  |  |  |  |  | 
| 358 | 20 | 100 | 100 |  |  | 99 | delete $params{algorithm} if defined $params{algorithm} && $params{algorithm} eq 'sha1'; | 
| 359 | 20 | 100 | 100 |  |  | 117 | delete $params{period}    if defined $params{period} && $params{period} == 30; | 
| 360 | 20 | 100 | 66 |  |  | 105 | delete $params{digits}    if defined $params{digits} && $params{digits} == 6; | 
| 361 | 20 | 100 | 100 |  |  | 83 | delete $params{counter}   if defined $params{counter} && $params{counter} == 0; | 
| 362 |  |  |  |  |  |  |  | 
| 363 | 20 |  |  |  |  | 98 | my $uri = "otpauth://$type/$label?secret=$secret&issuer=$issuer"; | 
| 364 |  |  |  |  |  |  |  | 
| 365 | 20 | 100 |  |  |  | 59 | if (defined $params{encoder}) { | 
| 366 | 1 |  |  |  |  | 4 | $uri .= "&encoder=$params{encoder}"; | 
| 367 | 1 |  |  |  |  | 13 | return $uri; | 
| 368 |  |  |  |  |  |  | } | 
| 369 | 19 | 100 |  |  |  | 53 | $uri .= '&algorithm=' . uc($params{algorithm}) if defined $params{algorithm}; | 
| 370 | 19 | 100 |  |  |  | 52 | $uri .= "&digits=$params{digits}"   if defined $params{digits}; | 
| 371 | 19 | 100 |  |  |  | 69 | $uri .= "&counter=$params{counter}" if defined $params{counter}; | 
| 372 | 19 | 100 |  |  |  | 50 | $uri .= "&period=$params{period}"   if defined $params{period}; | 
| 373 |  |  |  |  |  |  |  | 
| 374 | 19 |  |  |  |  | 206 | return $uri; | 
| 375 |  |  |  |  |  |  | } | 
| 376 |  |  |  |  |  |  |  | 
| 377 |  |  |  |  |  |  | sub _hotp_params { | 
| 378 | 36 |  |  | 36 |  | 89 | my $self = shift; | 
| 379 |  |  |  |  |  |  |  | 
| 380 | 36 |  | 100 |  |  | 142 | my %params = ( | 
|  |  |  | 100 |  |  |  |  | 
|  |  |  | 100 |  |  |  |  | 
| 381 |  |  |  |  |  |  | type    => 'hotp', | 
| 382 |  |  |  |  |  |  | issuer  => $self->expand_title      || 'KDBX', | 
| 383 |  |  |  |  |  |  | account => $self->expand_username   || 'none', | 
| 384 |  |  |  |  |  |  | digits  => 6, | 
| 385 |  |  |  |  |  |  | counter => $self->string_value('HmacOtp-Counter') // 0, | 
| 386 |  |  |  |  |  |  | $self->_otp_secret_params('Hmac'), | 
| 387 |  |  |  |  |  |  | ); | 
| 388 | 36 | 100 |  |  |  | 1132 | return %params if $params{secret}; | 
| 389 |  |  |  |  |  |  |  | 
| 390 | 12 |  |  |  |  | 46 | my %otp_params = $self->_otp_params; | 
| 391 | 12 | 50 | 33 |  |  | 79 | return () if !$otp_params{secret} || $otp_params{type} ne 'hotp'; | 
| 392 |  |  |  |  |  |  |  | 
| 393 |  |  |  |  |  |  | # $otp_params{counter} = 0 | 
| 394 |  |  |  |  |  |  |  | 
| 395 | 12 |  |  |  |  | 87 | return (%params, %otp_params); | 
| 396 |  |  |  |  |  |  | } | 
| 397 |  |  |  |  |  |  |  | 
| 398 |  |  |  |  |  |  | sub _totp_params { | 
| 399 | 21 |  |  | 21 |  | 36 | my $self = shift; | 
| 400 |  |  |  |  |  |  |  | 
| 401 | 21 |  |  |  |  | 112 | my %algorithms = ( | 
| 402 |  |  |  |  |  |  | 'HMAC-SHA-1'    => 'sha1', | 
| 403 |  |  |  |  |  |  | 'HMAC-SHA-256'  => 'sha256', | 
| 404 |  |  |  |  |  |  | 'HMAC-SHA-512'  => 'sha512', | 
| 405 |  |  |  |  |  |  | ); | 
| 406 |  |  |  |  |  |  | my %params = ( | 
| 407 |  |  |  |  |  |  | type        => 'totp', | 
| 408 |  |  |  |  |  |  | issuer      => $self->expand_title      || 'KDBX', | 
| 409 |  |  |  |  |  |  | account     => $self->expand_username   || 'none', | 
| 410 |  |  |  |  |  |  | digits      => $self->string_value('TimeOtp-Length') // 6, | 
| 411 | 21 |  | 100 |  |  | 95 | algorithm   => $algorithms{$self->string_value('TimeOtp-Algorithm') || ''} || 'sha1', | 
|  |  |  | 100 |  |  |  |  | 
|  |  |  | 100 |  |  |  |  | 
|  |  |  | 100 |  |  |  |  | 
|  |  |  | 100 |  |  |  |  | 
| 412 |  |  |  |  |  |  | period      => $self->string_value('TimeOtp-Period') // 30, | 
| 413 |  |  |  |  |  |  | $self->_otp_secret_params('Time'), | 
| 414 |  |  |  |  |  |  | ); | 
| 415 | 21 | 100 |  |  |  | 133 | return %params if $params{secret}; | 
| 416 |  |  |  |  |  |  |  | 
| 417 | 10 |  |  |  |  | 35 | my %otp_params = $self->_otp_params; | 
| 418 | 10 | 50 | 33 |  |  | 70 | return () if !$otp_params{secret} || $otp_params{type} ne 'totp'; | 
| 419 |  |  |  |  |  |  |  | 
| 420 | 10 |  |  |  |  | 93 | return (%params, %otp_params); | 
| 421 |  |  |  |  |  |  | } | 
| 422 |  |  |  |  |  |  |  | 
| 423 |  |  |  |  |  |  | # KeePassXC style | 
| 424 |  |  |  |  |  |  | sub _otp_params { | 
| 425 | 22 |  |  | 22 |  | 47 | my $self = shift; | 
| 426 | 22 |  |  |  |  | 80 | load_optional('Pass::OTP::URI'); | 
| 427 |  |  |  |  |  |  |  | 
| 428 | 22 |  | 50 |  |  | 69 | my $uri = $self->string_value('otp') || ''; | 
| 429 | 22 |  |  |  |  | 39 | my %params; | 
| 430 | 22 | 50 |  |  |  | 195 | %params = Pass::OTP::URI::parse($uri) if $uri =~ m!^otpauth://!; | 
| 431 | 22 | 50 | 33 |  |  | 900 | return () if !$params{secret} || !$params{type}; | 
| 432 |  |  |  |  |  |  |  | 
| 433 | 22 | 100 | 100 |  |  | 114 | if (($params{encoder} // '') eq 'steam') { | 
| 434 | 2 |  |  |  |  | 5 | $params{digits} = 5; | 
| 435 | 2 |  |  |  |  | 5 | $params{chars}  = '23456789BCDFGHJKMNPQRTVWXY'; | 
| 436 |  |  |  |  |  |  | } | 
| 437 |  |  |  |  |  |  |  | 
| 438 |  |  |  |  |  |  | # Pass::OTP::URI doesn't provide the issuer and account separately, so get them from the label | 
| 439 | 22 |  | 50 |  |  | 95 | my ($issuer, $user) = split(':', $params{label} // ':', 2); | 
| 440 | 22 |  | 66 |  |  | 80 | $params{issuer}  //= uri_unescape_utf8($issuer); | 
| 441 | 22 |  | 33 |  |  | 353 | $params{account} //= uri_unescape_utf8($user); | 
| 442 |  |  |  |  |  |  |  | 
| 443 | 22 | 100 |  |  |  | 1797 | $params{algorithm}  = lc($params{algorithm}) if $params{algorithm}; | 
| 444 | 22 | 100 |  |  |  | 95 | $params{counter}    = $self->string_value('HmacOtp-Counter') if $params{type} eq 'hotp'; | 
| 445 |  |  |  |  |  |  |  | 
| 446 | 22 |  |  |  |  | 179 | return %params; | 
| 447 |  |  |  |  |  |  | } | 
| 448 |  |  |  |  |  |  |  | 
| 449 |  |  |  |  |  |  | sub _otp_secret_params { | 
| 450 | 57 |  |  | 57 |  | 116 | my $self = shift; | 
| 451 | 57 |  | 50 |  |  | 139 | my $type = shift // return (); | 
| 452 |  |  |  |  |  |  |  | 
| 453 | 57 |  |  |  |  | 163 | my $secret_txt = $self->string_value("${type}Otp-Secret"); | 
| 454 | 57 |  |  |  |  | 169 | my $secret_hex = $self->string_value("${type}Otp-Secret-Hex"); | 
| 455 | 57 |  |  |  |  | 183 | my $secret_b32 = $self->string_value("${type}Otp-Secret-Base32"); | 
| 456 | 57 |  |  |  |  | 191 | my $secret_b64 = $self->string_value("${type}Otp-Secret-Base64"); | 
| 457 |  |  |  |  |  |  |  | 
| 458 | 57 |  |  |  |  | 144 | my $count = grep { defined } ($secret_txt, $secret_hex, $secret_b32, $secret_b64); | 
|  | 228 |  |  |  |  | 344 |  | 
| 459 | 57 | 100 |  |  |  | 316 | return () if $count == 0; | 
| 460 | 35 | 100 |  |  |  | 127 | alert "Found multiple ${type}Otp-Secret strings", count => $count if 1 < $count; | 
| 461 |  |  |  |  |  |  |  | 
| 462 | 35 | 100 |  |  |  | 206 | return (secret => $secret_b32, base32 => 1) if defined $secret_b32; | 
| 463 | 20 | 100 |  |  |  | 91 | return (secret => decode_b64($secret_b64))  if defined $secret_b64; | 
| 464 | 16 | 100 |  |  |  | 96 | return (secret => pack('H*', $secret_hex))  if defined $secret_hex; | 
| 465 | 12 |  |  |  |  | 88 | return (secret => encode('UTF-8', $secret_txt)); | 
| 466 |  |  |  |  |  |  | } | 
| 467 |  |  |  |  |  |  |  | 
| 468 |  |  |  |  |  |  | sub _hotp_increment_counter { | 
| 469 | 27 |  |  | 27 |  | 56 | my $self    = shift; | 
| 470 | 27 |  | 100 |  |  | 117 | my $counter = shift // $self->string_value('HmacOtp-Counter') || 0; | 
| 471 |  |  |  |  |  |  |  | 
| 472 | 27 | 50 |  |  |  | 136 | looks_like_number($counter) or throw 'HmacOtp-Counter value must be a number', value => $counter; | 
| 473 | 27 |  |  |  |  | 53 | my $next = $counter + 1; | 
| 474 | 27 |  |  |  |  | 80 | $self->string('HmacOtp-Counter', $next); | 
| 475 | 27 |  |  |  |  | 49 | return $next; | 
| 476 |  |  |  |  |  |  | } | 
| 477 |  |  |  |  |  |  |  | 
| 478 |  |  |  |  |  |  | ############################################################################## | 
| 479 |  |  |  |  |  |  |  | 
| 480 |  |  |  |  |  |  |  | 
| 481 |  |  |  |  |  |  | sub size { | 
| 482 | 3 |  |  | 3 | 1 | 5 | my $self = shift; | 
| 483 |  |  |  |  |  |  |  | 
| 484 | 3 |  |  |  |  | 3 | my $size = 0; | 
| 485 |  |  |  |  |  |  |  | 
| 486 |  |  |  |  |  |  | # tags | 
| 487 | 3 |  | 50 |  |  | 6 | $size += length(encode('UTF-8', $self->tags // '')); | 
| 488 |  |  |  |  |  |  |  | 
| 489 |  |  |  |  |  |  | # attributes (strings) | 
| 490 | 3 |  |  |  |  | 258 | while (my ($key, $string) = each %{$self->strings}) { | 
|  | 18 |  |  |  |  | 901 |  | 
| 491 | 15 | 50 |  |  |  | 51 | next if !defined $string->{value}; | 
| 492 | 15 |  | 50 |  |  | 27 | $size += length(encode('UTF-8', $key)) + length(encode('UTF-8', $string->{value} // '')); | 
| 493 |  |  |  |  |  |  | } | 
| 494 |  |  |  |  |  |  |  | 
| 495 |  |  |  |  |  |  | # custom data | 
| 496 | 3 |  |  |  |  | 6 | while (my ($key, $item) = each %{$self->custom_data}) { | 
|  | 3 |  |  |  |  | 9 |  | 
| 497 | 0 | 0 |  |  |  | 0 | next if !defined $item->{value}; | 
| 498 | 0 |  | 0 |  |  | 0 | $size += length(encode('UTF-8', $key)) + length(encode('UTF-8', $item->{value} // '')); | 
| 499 |  |  |  |  |  |  | } | 
| 500 |  |  |  |  |  |  |  | 
| 501 |  |  |  |  |  |  | # binaries | 
| 502 | 3 |  |  |  |  | 4 | while (my ($key, $binary) = each %{$self->binaries}) { | 
|  | 3 |  |  |  |  | 6 |  | 
| 503 | 0 | 0 |  |  |  | 0 | next if !defined $binary->{value}; | 
| 504 |  |  |  |  |  |  | my $value_len = utf8::is_utf8($binary->{value}) ? length(encode('UTF-8', $binary->{value})) | 
| 505 | 0 | 0 |  |  |  | 0 | : length($binary->{value}); | 
| 506 | 0 |  |  |  |  | 0 | $size += length(encode('UTF-8', $key)) + $value_len; | 
| 507 |  |  |  |  |  |  | } | 
| 508 |  |  |  |  |  |  |  | 
| 509 |  |  |  |  |  |  | # autotype associations | 
| 510 | 3 | 50 |  |  |  | 4 | for my $association (@{$self->auto_type->{associations} || []}) { | 
|  | 3 |  |  |  |  | 5 |  | 
| 511 |  |  |  |  |  |  | $size += length(encode('UTF-8', $association->{window})) | 
| 512 | 0 |  | 0 |  |  | 0 | + length(encode('UTF-8', $association->{keystroke_sequence} // '')); | 
| 513 |  |  |  |  |  |  | } | 
| 514 |  |  |  |  |  |  |  | 
| 515 | 3 |  |  |  |  | 12 | return $size; | 
| 516 |  |  |  |  |  |  | } | 
| 517 |  |  |  |  |  |  |  | 
| 518 |  |  |  |  |  |  | ############################################################################## | 
| 519 |  |  |  |  |  |  |  | 
| 520 |  |  |  |  |  |  | sub history { | 
| 521 | 284 |  |  | 284 | 1 | 425 | my $self = shift; | 
| 522 | 284 |  | 100 |  |  | 740 | my $entries = $self->{history} //= []; | 
| 523 | 284 | 100 | 100 |  |  | 719 | if (@$entries && !blessed($entries->[0])) { | 
| 524 | 4 |  |  |  |  | 12 | @$entries = map { $self->_wrap_entry($_, $self->kdbx) } @$entries; | 
|  | 6 |  |  |  |  | 18 |  | 
| 525 |  |  |  |  |  |  | } | 
| 526 | 284 |  |  | 0 |  | 1142 | assert { !any { !blessed $_ } @$entries }; | 
|  | 0 |  |  |  |  | 0 |  | 
|  | 0 |  |  |  |  | 0 |  | 
| 527 | 284 |  |  |  |  | 1088 | return $entries; | 
| 528 |  |  |  |  |  |  | } | 
| 529 |  |  |  |  |  |  |  | 
| 530 |  |  |  |  |  |  |  | 
| 531 |  |  |  |  |  |  | sub history_size { | 
| 532 | 2 |  |  | 2 | 1 | 4 | my $self = shift; | 
| 533 | 2 |  |  |  |  | 3 | return sum0 map { $_->size } @{$self->history}; | 
|  | 3 |  |  |  |  | 8 |  | 
|  | 2 |  |  |  |  | 3 |  | 
| 534 |  |  |  |  |  |  | } | 
| 535 |  |  |  |  |  |  |  | 
| 536 |  |  |  |  |  |  |  | 
| 537 |  |  |  |  |  |  | sub prune_history { | 
| 538 | 2 |  |  | 2 | 1 | 4 | my $self = shift; | 
| 539 | 2 |  |  |  |  | 6 | my %args = @_; | 
| 540 |  |  |  |  |  |  |  | 
| 541 | 2 |  | 33 |  |  | 10 | my $max_items = $args{max_items} // eval { $self->kdbx->history_max_items } // HISTORY_DEFAULT_MAX_ITEMS; | 
|  | 0 |  | 50 |  |  | 0 |  | 
| 542 | 2 |  | 33 |  |  | 6 | my $max_size  = $args{max_size}  // eval { $self->kdbx->history_max_size }  // HISTORY_DEFAULT_MAX_SIZE; | 
|  | 0 |  | 50 |  |  | 0 |  | 
| 543 | 2 |  | 33 |  |  | 8 | my $max_age   = $args{max_age}   // eval { $self->kdbx->maintenance_history_days } // HISTORY_DEFAULT_MAX_AGE; | 
|  | 0 |  | 50 |  |  | 0 |  | 
| 544 |  |  |  |  |  |  |  | 
| 545 |  |  |  |  |  |  | # history is ordered oldest to newest | 
| 546 | 2 |  |  |  |  | 5 | my $history = $self->history; | 
| 547 |  |  |  |  |  |  |  | 
| 548 | 2 |  |  |  |  | 4 | my @removed; | 
| 549 |  |  |  |  |  |  |  | 
| 550 | 2 | 50 | 33 |  |  | 8 | if (0 <= $max_items && $max_items < @$history) { | 
| 551 | 0 |  |  |  |  | 0 | push @removed, splice @$history, -$max_items; | 
| 552 |  |  |  |  |  |  | } | 
| 553 |  |  |  |  |  |  |  | 
| 554 | 2 | 50 |  |  |  | 5 | if (0 <= $max_size) { | 
| 555 | 2 |  |  |  |  | 5 | my $current_size = $self->history_size; | 
| 556 | 2 |  |  |  |  | 6 | while ($max_size < $current_size) { | 
| 557 | 0 |  |  |  |  | 0 | push @removed, my $entry = shift @$history; | 
| 558 | 0 |  |  |  |  | 0 | $current_size -= $entry->size; | 
| 559 |  |  |  |  |  |  | } | 
| 560 |  |  |  |  |  |  | } | 
| 561 |  |  |  |  |  |  |  | 
| 562 | 2 | 50 |  |  |  | 6 | if (0 <= $max_age) { | 
| 563 | 2 |  |  |  |  | 8 | my $cutoff = gmtime - ($max_age * 86400); | 
| 564 | 2 |  |  |  |  | 115 | for (my $i = @$history - 1; 0 <= $i; --$i) { | 
| 565 | 3 |  |  |  |  | 35 | my $entry = $history->[$i]; | 
| 566 | 3 | 100 |  |  |  | 7 | next if $cutoff <= $entry->last_modification_time; | 
| 567 | 1 |  |  |  |  | 31 | push @removed, splice @$history, $i, 1; | 
| 568 |  |  |  |  |  |  | } | 
| 569 |  |  |  |  |  |  | } | 
| 570 |  |  |  |  |  |  |  | 
| 571 | 2 |  |  |  |  | 50 | @removed = sort { $a->last_modification_time <=> $b->last_modification_time } @removed; | 
|  | 0 |  |  |  |  | 0 |  | 
| 572 | 2 |  |  |  |  | 8 | return @removed; | 
| 573 |  |  |  |  |  |  | } | 
| 574 |  |  |  |  |  |  |  | 
| 575 |  |  |  |  |  |  |  | 
| 576 |  |  |  |  |  |  | sub add_historical_entry { | 
| 577 | 8 |  |  | 8 | 1 | 12 | my $self = shift; | 
| 578 | 8 |  |  |  |  | 42 | delete $_->{history} for @_; | 
| 579 | 8 |  | 100 |  |  | 11 | push @{$self->{history} //= []}, map { $self->_wrap_entry($_) } @_; | 
|  | 8 |  |  |  |  | 30 |  | 
|  | 8 |  |  |  |  | 88 |  | 
| 580 |  |  |  |  |  |  | } | 
| 581 |  |  |  |  |  |  |  | 
| 582 |  |  |  |  |  |  |  | 
| 583 |  |  |  |  |  |  | sub remove_historical_entry { | 
| 584 | 34 |  |  | 34 | 1 | 48 | my $self    = shift; | 
| 585 | 34 |  |  |  |  | 40 | my $entry   = shift; | 
| 586 | 34 |  |  |  |  | 64 | my $history = $self->history; | 
| 587 |  |  |  |  |  |  |  | 
| 588 | 34 |  |  |  |  | 56 | my @removed; | 
| 589 | 34 |  |  |  |  | 112 | for (my $i = @$history - 1; 0 <= $i; --$i) { | 
| 590 | 3 |  |  |  |  | 4 | my $item = $history->[$i]; | 
| 591 | 3 | 50 |  |  |  | 14 | next if Hash::Util::FieldHash::id($entry) != Hash::Util::FieldHash::id($item); | 
| 592 | 0 |  |  |  |  | 0 | push @removed, splice @{$self->{history}}, $i, 1; | 
|  | 0 |  |  |  |  | 0 |  | 
| 593 |  |  |  |  |  |  | } | 
| 594 | 34 |  |  |  |  | 77 | return @removed; | 
| 595 |  |  |  |  |  |  | } | 
| 596 |  |  |  |  |  |  |  | 
| 597 |  |  |  |  |  |  |  | 
| 598 |  |  |  |  |  |  | sub current_entry { | 
| 599 | 51 |  |  | 51 | 1 | 74 | my $self    = shift; | 
| 600 | 51 |  |  |  |  | 123 | my $parent  = $self->group; | 
| 601 |  |  |  |  |  |  |  | 
| 602 | 51 | 100 |  |  |  | 116 | if ($parent) { | 
| 603 | 9 |  |  |  |  | 18 | my $id = $self->uuid; | 
| 604 | 9 |  |  | 9 |  | 22 | my $entry = first { $id eq $_->uuid } @{$parent->entries}; | 
|  | 9 |  |  |  |  | 14 |  | 
|  | 9 |  |  |  |  | 20 |  | 
| 605 | 9 | 50 |  |  |  | 33 | return $entry if $entry; | 
| 606 |  |  |  |  |  |  | } | 
| 607 |  |  |  |  |  |  |  | 
| 608 | 42 |  |  |  |  | 69 | return $self; | 
| 609 |  |  |  |  |  |  | } | 
| 610 |  |  |  |  |  |  |  | 
| 611 |  |  |  |  |  |  |  | 
| 612 |  |  |  |  |  |  | sub is_current { | 
| 613 | 16 |  |  | 16 | 1 | 24 | my $self    = shift; | 
| 614 | 16 |  |  |  |  | 35 | my $current = $self->current_entry; | 
| 615 | 16 |  |  |  |  | 91 | return Hash::Util::FieldHash::id($self) == Hash::Util::FieldHash::id($current); | 
| 616 |  |  |  |  |  |  | } | 
| 617 |  |  |  |  |  |  |  | 
| 618 |  |  |  |  |  |  |  | 
| 619 | 1 |  |  | 1 | 1 | 3 | sub is_historical { !$_[0]->is_current } | 
| 620 |  |  |  |  |  |  |  | 
| 621 |  |  |  |  |  |  |  | 
| 622 |  |  |  |  |  |  | sub remove { | 
| 623 | 34 |  |  | 34 | 1 | 55 | my $self    = shift; | 
| 624 | 34 |  |  |  |  | 81 | my $current = $self->current_entry; | 
| 625 | 34 | 50 |  |  |  | 94 | return $self if $current->remove_historical_entry($self); | 
| 626 | 34 |  |  |  |  | 110 | $self->SUPER::remove(@_); | 
| 627 |  |  |  |  |  |  | } | 
| 628 |  |  |  |  |  |  |  | 
| 629 |  |  |  |  |  |  | ############################################################################## | 
| 630 |  |  |  |  |  |  |  | 
| 631 |  |  |  |  |  |  |  | 
| 632 |  |  |  |  |  |  | sub searching_enabled { | 
| 633 | 0 |  |  | 0 | 1 | 0 | my $self = shift; | 
| 634 | 0 |  |  |  |  | 0 | my $parent = $self->group; | 
| 635 | 0 | 0 |  |  |  | 0 | return $parent->effective_enable_searching if $parent; | 
| 636 | 0 |  |  |  |  | 0 | return true; | 
| 637 |  |  |  |  |  |  | } | 
| 638 |  |  |  |  |  |  |  | 
| 639 |  |  |  |  |  |  | sub auto_type_enabled { | 
| 640 | 85 |  |  | 85 | 1 | 130 | my $self = shift; | 
| 641 | 85 | 50 |  |  |  | 184 | $self->auto_type->{enabled} = to_bool(shift) if @_; | 
| 642 | 85 |  | 33 |  |  | 219 | $self->auto_type->{enabled} //= true; | 
| 643 | 85 | 50 |  |  |  | 846 | return false if !$self->auto_type->{enabled}; | 
| 644 | 85 | 100 |  |  |  | 687 | return true if !$self->is_connected; | 
| 645 | 26 |  |  |  |  | 90 | my $parent = $self->group; | 
| 646 | 26 | 50 |  |  |  | 65 | return $parent->effective_enable_auto_type if $parent; | 
| 647 | 26 |  |  |  |  | 64 | return true; | 
| 648 |  |  |  |  |  |  | } | 
| 649 |  |  |  |  |  |  |  | 
| 650 |  |  |  |  |  |  | ############################################################################## | 
| 651 |  |  |  |  |  |  |  | 
| 652 |  |  |  |  |  |  | sub _signal { | 
| 653 | 46 |  |  | 46 |  | 63 | my $self = shift; | 
| 654 | 46 |  |  |  |  | 60 | my $type = shift; | 
| 655 | 46 |  |  |  |  | 175 | return $self->SUPER::_signal("entry.$type", @_); | 
| 656 |  |  |  |  |  |  | } | 
| 657 |  |  |  |  |  |  |  | 
| 658 |  |  |  |  |  |  | sub _commit { | 
| 659 | 8 |  |  | 8 |  | 13 | my $self = shift; | 
| 660 | 8 |  |  |  |  | 12 | my $orig = shift; | 
| 661 | 8 |  |  |  |  | 27 | $self->add_historical_entry($orig); | 
| 662 | 8 |  |  |  |  | 27 | my $time = gmtime; | 
| 663 | 8 |  |  |  |  | 462 | $self->last_modification_time($time); | 
| 664 | 8 |  |  |  |  | 93 | $self->last_access_time($time); | 
| 665 |  |  |  |  |  |  | } | 
| 666 |  |  |  |  |  |  |  | 
| 667 | 138 |  |  | 138 | 1 | 377 | sub label { shift->expand_title(@_) } | 
| 668 |  |  |  |  |  |  |  | 
| 669 |  |  |  |  |  |  | ### Name of the parent attribute expected to contain the object | 
| 670 | 22 |  |  | 22 |  | 41 | sub _parent_container { 'entries' } | 
| 671 |  |  |  |  |  |  |  | 
| 672 |  |  |  |  |  |  | 1; | 
| 673 |  |  |  |  |  |  |  | 
| 674 |  |  |  |  |  |  | __END__ |