]> Dogcows Code - chaz/p5-File-KDBX/blobdiff - lib/File/KDBX/Entry.pm
add initial WIP
[chaz/p5-File-KDBX] / lib / File / KDBX / Entry.pm
diff --git a/lib/File/KDBX/Entry.pm b/lib/File/KDBX/Entry.pm
new file mode 100644 (file)
index 0000000..c3ddcb9
--- /dev/null
@@ -0,0 +1,1060 @@
+package File::KDBX::Entry;
+# ABSTRACT: A KDBX database entry
+
+use warnings;
+use strict;
+
+use Crypt::Misc 0.029 qw(encode_b32r decode_b64);
+use Devel::GlobalDestruction;
+use Encode qw(encode);
+use File::KDBX::Constants qw(:history :icon);
+use File::KDBX::Error;
+use File::KDBX::Util qw(:function :uri generate_uuid load_optional);
+use List::Util qw(sum0);
+use Ref::Util qw(is_plain_hashref is_ref);
+use Scalar::Util qw(looks_like_number refaddr);
+use Storable qw(dclone);
+use Time::Piece;
+use boolean;
+use namespace::clean;
+
+use parent 'File::KDBX::Object';
+
+our $VERSION = '999.999'; # VERSION
+
+my $PLACEHOLDER_MAX_DEPTH = 10;
+my %PLACEHOLDERS;
+my %STANDARD_STRINGS = map { $_ => 1 } qw(Title UserName Password URL Notes);
+
+=attr uuid
+
+128-bit UUID identifying the entry within the database.
+
+=attr icon_id
+
+Integer representing a default icon. See L<File::KDBX::Constants/":icon"> for valid values.
+
+=attr custom_icon_uuid
+
+128-bit UUID identifying a custom icon within the database.
+
+=attr foreground_color
+
+Text color represented as a string of the form C<#000000>.
+
+=attr background_color
+
+Background color represented as a string of the form C<#FFFFFF>.
+
+=attr override_url
+
+TODO
+
+=attr tags
+
+Text string with arbitrary tags which can be used to build a taxonomy.
+
+=attr auto_type
+
+Auto-type details.
+
+    {
+        enabled                     => true,
+        data_transfer_obfuscation   => 0,
+        default_sequence            => '{USERNAME}{TAB}{PASSWORD}{ENTER}',
+        associations                => [
+            {
+                window              => 'My Bank - Mozilla Firefox',
+                keystroke_sequence  => '{PASSWORD}{ENTER}',
+            },
+        ],
+    }
+
+=attr previous_parent_group
+
+128-bit UUID identifying a group within the database.
+
+=attr quality_check
+
+Boolean indicating whether the entry password should be tested for weakness and show up in reports.
+
+=attr strings
+
+Hash with entry strings, including the standard strings as well as any custom ones.
+
+    {
+        # Every entry has these five strings:
+        Title    => { value => 'Example Entry' },
+        UserName => { value => 'jdoe' },
+        Password => { value => 's3cr3t', protect => true },
+        URL      => { value => 'https://example.com' }
+        Notes    => { value => '' },
+        # May also have custom strings:
+        MySystem => { value => 'The mainframe' },
+    }
+
+=attr binaries
+
+Files or attachments.
+
+=attr custom_data
+
+A set of key-value pairs used to store arbitrary data, usually used by software to keep track of state rather
+than by end users (who typically work with the strings and binaries).
+
+=attr history
+
+Array of historical entries. Historical entries are prior versions of the same entry so they all share the
+same UUID with the current entry.
+
+=attr last_modification_time
+
+Date and time when the entry was last modified.
+
+=attr creation_time
+
+Date and time when the entry was created.
+
+=attr last_access_time
+
+Date and time when the entry was last accessed.
+
+=attr expiry_time
+
+Date and time when the entry expired or will expire.
+
+=attr expires
+
+Boolean value indicating whether or not an entry is expired.
+
+=attr usage_count
+
+The number of times an entry has been used, which typically means how many times the C<Password> string has
+been accessed.
+
+=attr location_changed
+
+Date and time when the entry was last moved to a different group.
+
+=attr notes
+
+Alias for the C<Notes> string value.
+
+=attr password
+
+Alias for the C<Password> string value.
+
+=attr title
+
+Alias for the C<Title> string value.
+
+=attr url
+
+Alias for the C<URL> string value.
+
+=attr username
+
+Aliases for the C<UserName> string value.
+
+=cut
+
+sub uuid {
+    my $self = shift;
+    if (@_ || !defined $self->{uuid}) {
+        my %args = @_ % 2 == 1 ? (uuid => shift, @_) : @_;
+        my $old_uuid = $self->{uuid};
+        my $uuid = $self->{uuid} = delete $args{uuid} // generate_uuid;
+        for my $entry (@{$self->history}) {
+            $entry->{uuid} = $uuid;
+        }
+        # if (defined $old_uuid and my $kdbx = $KDBX{refaddr($self)}) {
+        #     $kdbx->_update_entry_uuid($old_uuid, $uuid, $self);
+        # }
+    }
+    $self->{uuid};
+}
+
+my @ATTRS = qw(uuid custom_data history);
+my %ATTRS = (
+    # uuid                    => sub { generate_uuid(printable => 1) },
+    icon_id                 => ICON_PASSWORD,
+    custom_icon_uuid        => undef,
+    foreground_color        => '',
+    background_color        => '',
+    override_url            => '',
+    tags                    => '',
+    auto_type               => sub { +{} },
+    previous_parent_group   => undef,
+    quality_check           => true,
+    strings                 => sub { +{} },
+    binaries                => sub { +{} },
+    # custom_data             => sub { +{} },
+    # history                 => sub { +[] },
+);
+my %ATTRS_TIMES = (
+    last_modification_time  => sub { gmtime },
+    creation_time           => sub { gmtime },
+    last_access_time        => sub { gmtime },
+    expiry_time             => sub { gmtime },
+    expires                 => false,
+    usage_count             => 0,
+    location_changed        => sub { gmtime },
+);
+my %ATTRS_STRINGS = (
+    title                   => 'Title',
+    username                => 'UserName',
+    password                => 'Password',
+    url                     => 'URL',
+    notes                   => 'Notes',
+);
+
+while (my ($attr, $default) = each %ATTRS) {
+    no strict 'refs'; ## no critic (ProhibitNoStrict)
+    *{$attr} = sub {
+        my $self = shift;
+        $self->{$attr} = shift if @_;
+        $self->{$attr} //= (ref $default eq 'CODE') ? $default->($self) : $default;
+    };
+}
+while (my ($attr, $default) = each %ATTRS_TIMES) {
+    no strict 'refs'; ## no critic (ProhibitNoStrict)
+    *{$attr} = sub {
+        my $self = shift;
+        $self->{times} //= {};
+        $self->{times}{$attr} = shift if @_;
+        $self->{times}{$attr} //= (ref $default eq 'CODE') ? $default->($self) : $default;
+    };
+}
+while (my ($attr, $string_key) = each %ATTRS_STRINGS) {
+    no strict 'refs'; ## no critic (ProhibitNoStrict)
+    *{$attr} = sub { shift->string_value($string_key, @_) };
+    *{"expanded_${attr}"} = sub { shift->expanded_string_value($string_key, @_) };
+}
+
+sub _set_default_attributes {
+    my $self = shift;
+    $self->$_ for @ATTRS, keys %ATTRS, keys %ATTRS_TIMES, keys %ATTRS_STRINGS;
+}
+
+sub init {
+    my $self = shift;
+    my %args = @_;
+
+    while (my ($key, $val) = each %args) {
+        if (my $method = $self->can($key)) {
+            $self->$method($val);
+        }
+        else {
+            $self->string($key => $val);
+        }
+    }
+
+    return $self;
+}
+
+sub label { shift->title(@_) }
+
+##############################################################################
+
+=method string
+
+    \%string = $entry->string($string_key);
+
+    $entry->string($string_key, \%string);
+    $entry->string($string_key, %attributes);
+    $entry->string($string_key, $value); # same as: value => $value
+
+Get or set a string. Every string has a unique (to the entry) key and flags and so are returned as a hash
+structure. For example:
+
+    $string = {
+        value   => 'Password',
+        protect => true,
+    };
+
+Every string should have a value and these optional flags which might exist:
+
+=for :list
+* C<protect> - Whether or not the string value should be memory-protected.
+
+=cut
+
+sub string {
+    my $self = shift;
+    # use Data::Dumper;
+    # $self->{strings} = shift if @_ == 1 && is_plain_hashref($_[0]);
+    # return $self->{strings} //= {} if !@_;
+
+    my %args = @_     == 2 ? (key => shift, value => shift)
+             : @_ % 2 == 1 ? (key => shift, @_) : @_;
+
+    if (!defined $args{key} && !defined $args{value}) {
+        my %standard = (value => 1, protect => 1);
+        my @other_keys = grep { !$standard{$_} } keys %args;
+        if (@other_keys == 1) {
+            my $key = $args{key} = $other_keys[0];
+            $args{value} = delete $args{$key};
+        }
+    }
+
+    my $key = delete $args{key} or throw 'Must provide a string key to access';
+
+    return $self->{strings}{$key} = $args{value} if is_plain_hashref($args{value});
+
+    while (my ($field, $value) = each %args) {
+        $self->{strings}{$key}{$field} = $value;
+    }
+
+    # Auto-vivify the standard strings.
+    if ($STANDARD_STRINGS{$key}) {
+        return $self->{strings}{$key} //= {value => '', $self->_protect($key) ? (protect => true) : ()};
+    }
+    return $self->{strings}{$key};
+}
+
+### Get whether or not a standard string is configured to be protected
+sub _protect {
+    my $self = shift;
+    my $key  = shift;
+    return false if !$STANDARD_STRINGS{$key};
+    if (my $kdbx = eval { $self->kdbx }) {
+        my $protect = $kdbx->memory_protection($key);
+        return $protect if defined $protect;
+    }
+    return $key eq 'Password';
+}
+
+=method string_value
+
+    $string = $entry->string_value;
+
+Access a string value directly. Returns C<undef> if the string is not set.
+
+=cut
+
+sub string_value {
+    my $self = shift;
+    my $string = $self->string(@_) // return undef;
+    return $string->{value};
+}
+
+=method expanded_string_value
+
+    $string = $entry->expanded_string_value;
+
+Same as L</string_value> but will substitute placeholders and resolve field references. Any placeholders that
+do not expand to values are left as-is.
+
+See L</Placeholders>.
+
+Some placeholders (notably field references) require the entry be associated with a database and will throw an
+error if there is no association.
+
+=cut
+
+sub _expand_placeholder {
+    my $self = shift;
+    my $placeholder = shift;
+    my $arg = shift;
+
+    require File::KDBX;
+
+    my $placeholder_key = $placeholder;
+    if (defined $arg) {
+        $placeholder_key = $File::KDBX::PLACEHOLDERS{"${placeholder}:${arg}"} ? "${placeholder}:${arg}"
+                                                                              : "${placeholder}:";
+    }
+    return if !defined $File::KDBX::PLACEHOLDERS{$placeholder_key};
+
+    my $local_key = join('/', refaddr($self), $placeholder_key);
+    local $PLACEHOLDERS{$local_key} = my $handler = $PLACEHOLDERS{$local_key} // do {
+        my $handler = $File::KDBX::PLACEHOLDERS{$placeholder_key} or next;
+        memoize recurse_limit($handler, $PLACEHOLDER_MAX_DEPTH, sub {
+            alert "Detected deep recursion while expanding $placeholder placeholder",
+                placeholder => $placeholder;
+            return; # undef
+        });
+    };
+
+    return $handler->($self, $arg, $placeholder);
+}
+
+sub _expand_string {
+    my $self    = shift;
+    my $str     = shift;
+
+    my $expand = memoize $self->can('_expand_placeholder'), $self;
+
+    # placeholders (including field references):
+    $str =~ s!\{([^:\}]+)(?::([^\}]*))?\}!$expand->(uc($1), $2, @_) // $&!egi;
+
+    # environment variables (alt syntax):
+    my $vars = join('|', map { quotemeta($_) } keys %ENV);
+    $str =~ s!\%($vars)\%!$expand->(ENV => $1, @_) // $&!eg;
+
+    return $str;
+}
+
+sub expanded_string_value {
+    my $self = shift;
+    my $str  = $self->string_value(@_) // return undef;
+    return $self->_expand_string($str);
+}
+
+=method other_strings
+
+    $other = $entry->other_strings;
+    $other = $entry->other_strings($delimiter);
+
+Get a concatenation of all non-standard string values. The default delimiter is a newline. This is is useful
+for executing queries to search for entities based on the contents of these other strings (if any).
+
+=cut
+
+sub other_strings {
+    my $self    = shift;
+    my $delim   = shift // "\n";
+
+    my @strings = map { $self->string_value($_) } grep { !$STANDARD_STRINGS{$_} } sort keys %{$self->strings};
+    return join($delim, @strings);
+}
+
+sub string_peek {
+    my $self = shift;
+    my $string = $self->string(@_);
+    return defined $string->{value} ? $string->{value} : $self->kdbx->peek($string);
+}
+
+sub password_peek { $_[0]->string_peek('Password') }
+
+##############################################################################
+
+sub binary {
+    my $self = shift;
+    my $key  = shift or throw 'Must provide a binary key to access';
+    if (@_) {
+        my $arg = @_ == 1 ? shift : undef;
+        my %args;
+        @args{keys %$arg} = values %$arg if ref $arg eq 'HASH';
+        $args{value} = $arg if !ref $arg;
+        while (my ($field, $value) = each %args) {
+            $self->{binaries}{$key}{$field} = $value;
+        }
+    }
+    my $binary = $self->{binaries}{$key} //= {value => ''};
+    if (defined (my $ref = $binary->{ref})) {
+        $binary = $self->{binaries}{$key} = dclone($self->kdbx->binaries->{$ref});
+    }
+    return $binary;
+}
+
+sub binary_novivify {
+    my $self = shift;
+    my $binary_key = shift;
+    return if !$self->{binaries}{$binary_key} && !@_;
+    return $self->binary($binary_key, @_);
+}
+
+sub binary_value {
+    my $self = shift;
+    my $binary = $self->binary_novivify(@_) // return undef;
+    return $binary->{value};
+}
+
+##############################################################################
+
+=method hmac_otp
+
+    $otp = $entry->hmac_otp(%options);
+
+Generate an HMAC-based one-time password, or C<undef> if HOTP is not configured for the entry. The entry's
+strings generally must first be unprotected, just like when accessing the password. Valid options are:
+
+=for :list
+* C<counter> - Specify the counter value
+
+To configure HOTP, see L</"One-time Passwords">.
+
+=cut
+
+sub hmac_otp {
+    my $self = shift;
+    load_optional('Pass::OTP');
+
+    my %params = ($self->_hotp_params, @_);
+    return if !defined $params{type} || !defined $params{secret};
+
+    $params{secret} = encode_b32r($params{secret}) if !$params{base32};
+    $params{base32} = 1;
+
+    my $otp = eval {Pass::OTP::otp(%params, @_) };
+    if (my $err = $@) {
+        throw 'Unable to generate HOTP', error => $err;
+    }
+
+    $self->_hotp_increment_counter($params{counter});
+
+    return $otp;
+}
+
+=method time_otp
+
+    $otp = $entry->time_otp(%options);
+
+Generate a time-based one-time password, or C<undef> if TOTP is not configured for the entry. The entry's
+strings generally must first be unprotected, just like when accessing the password. Valid options are:
+
+=for :list
+* C<now> - Specify the value for determining the time-step counter
+
+To configure TOTP, see L</"One-time Passwords">.
+
+=cut
+
+sub time_otp {
+    my $self = shift;
+    load_optional('Pass::OTP');
+
+    my %params = ($self->_totp_params, @_);
+    return if !defined $params{type} || !defined $params{secret};
+
+    $params{secret} = encode_b32r($params{secret}) if !$params{base32};
+    $params{base32} = 1;
+
+    my $otp = eval {Pass::OTP::otp(%params, @_) };
+    if (my $err = $@) {
+        throw 'Unable to generate TOTP', error => $err;
+    }
+
+    return $otp;
+}
+
+=method hmac_otp_uri
+
+=method time_otp_uri
+
+    $uri_string = $entry->hmac_otp_uri;
+    $uri_string = $entry->time_otp_uri;
+
+Get a HOTP or TOTP otpauth URI for the entry, if available.
+
+To configure OTP, see L</"One-time Passwords">.
+
+=cut
+
+sub hmac_otp_uri { $_[0]->_otp_uri($_[0]->_hotp_params) }
+sub time_otp_uri { $_[0]->_otp_uri($_[0]->_totp_params) }
+
+sub _otp_uri {
+    my $self = shift;
+    my %params = @_;
+
+    return if 4 != grep { defined } @params{qw(type secret issuer account)};
+    return if $params{type} !~ /^[ht]otp$/i;
+
+    my $label = delete $params{label};
+    $params{$_} = uri_escape_utf8($params{$_}) for keys %params;
+
+    my $type    = lc($params{type});
+    my $issuer  = $params{issuer};
+    my $account = $params{account};
+
+    $label //= "$issuer:$account";
+
+    my $secret = $params{secret};
+    $secret = uc(encode_b32r($secret)) if !$params{base32};
+
+    delete $params{algorithm} if defined $params{algorithm} && $params{algorithm} eq 'sha1';
+    delete $params{period}    if defined $params{period} && $params{period} == 30;
+    delete $params{digits}    if defined $params{digits} && $params{digits} == 6;
+    delete $params{counter}   if defined $params{counter} && $params{counter} == 0;
+
+    my $uri = "otpauth://$type/$label?secret=$secret&issuer=$issuer";
+
+    if (defined $params{encoder}) {
+        $uri .= "&encoder=$params{encoder}";
+        return $uri;
+    }
+    $uri .= '&algorithm=' . uc($params{algorithm}) if defined $params{algorithm};
+    $uri .= "&digits=$params{digits}"   if defined $params{digits};
+    $uri .= "&counter=$params{counter}" if defined $params{counter};
+    $uri .= "&period=$params{period}"   if defined $params{period};
+
+    return $uri;
+}
+
+sub _hotp_params {
+    my $self = shift;
+
+    my %params = (
+        type    => 'hotp',
+        issuer  => $self->title     || 'KDBX',
+        account => $self->username  || 'none',
+        digits  => 6,
+        counter => $self->string_value('HmacOtp-Counter') // 0,
+        $self->_otp_secret_params('Hmac'),
+    );
+    return %params if $params{secret};
+
+    my %otp_params = $self->_otp_params;
+    return () if !$otp_params{secret} || $otp_params{type} ne 'hotp';
+
+    # $otp_params{counter} = 0
+
+    return (%params, %otp_params);
+}
+
+sub _totp_params {
+    my $self = shift;
+
+    my %algorithms = (
+        'HMAC-SHA-1'    => 'sha1',
+        'HMAC-SHA-256'  => 'sha256',
+        'HMAC-SHA-512'  => 'sha512',
+    );
+    my %params = (
+        type        => 'totp',
+        issuer      => $self->title     || 'KDBX',
+        account     => $self->username  || 'none',
+        digits      => $self->string_value('TimeOtp-Length') // 6,
+        algorithm   => $algorithms{$self->string_value('TimeOtp-Algorithm') || ''} || 'sha1',
+        period      => $self->string_value('TimeOtp-Period') // 30,
+        $self->_otp_secret_params('Time'),
+    );
+    return %params if $params{secret};
+
+    my %otp_params = $self->_otp_params;
+    return () if !$otp_params{secret} || $otp_params{type} ne 'totp';
+
+    return (%params, %otp_params);
+}
+
+# KeePassXC style
+sub _otp_params {
+    my $self = shift;
+    load_optional('Pass::OTP::URI');
+
+    my $uri = $self->string_value('otp') || '';
+    my %params;
+    %params = Pass::OTP::URI::parse($uri) if $uri =~ m!^otpauth://!;
+    return () if !$params{secret} || !$params{type};
+
+    if (($params{encoder} // '') eq 'steam') {
+        $params{digits} = 5;
+        $params{chars}  = '23456789BCDFGHJKMNPQRTVWXY';
+    }
+
+    # Pass::OTP::URI doesn't provide the issuer and account separately, so get them from the label
+    my ($issuer, $user) = split(':', $params{label} // ':', 2);
+    $params{issuer}  //= uri_unescape_utf8($issuer);
+    $params{account} //= uri_unescape_utf8($user);
+
+    $params{algorithm}  = lc($params{algorithm}) if $params{algorithm};
+    $params{counter}    = $self->string_value('HmacOtp-Counter') if $params{type} eq 'hotp';
+
+    return %params;
+}
+
+sub _otp_secret_params {
+    my $self = shift;
+    my $type = shift // return ();
+
+    my $secret_txt = $self->string_value("${type}Otp-Secret");
+    my $secret_hex = $self->string_value("${type}Otp-Secret-Hex");
+    my $secret_b32 = $self->string_value("${type}Otp-Secret-Base32");
+    my $secret_b64 = $self->string_value("${type}Otp-Secret-Base64");
+
+    my $count = grep { defined } ($secret_txt, $secret_hex, $secret_b32, $secret_b64);
+    return () if $count == 0;
+    alert "Found multiple ${type}Otp-Secret strings", count => $count if 1 < $count;
+
+    return (secret => $secret_b32, base32 => 1) if defined $secret_b32;
+    return (secret => decode_b64($secret_b64))  if defined $secret_b64;
+    return (secret => pack('H*', $secret_hex))  if defined $secret_hex;
+    return (secret => encode('UTF-8', $secret_txt));
+}
+
+sub _hotp_increment_counter {
+    my $self    = shift;
+    my $counter = shift // $self->string_value('HmacOtp-Counter') || 0;
+
+    looks_like_number($counter) or throw 'HmacOtp-Counter value must be a number', value => $counter;
+    my $next = $counter + 1;
+    $self->string('HmacOtp-Counter', $next);
+    return $next;
+}
+
+##############################################################################
+
+=method size
+
+    $size = $entry->size;
+
+Get the size (in bytes) of an entry.
+
+B<NOTE:> This is not an exact figure because there is no canonical serialization of an entry. This size should
+only be used as a rough estimate for comparison with other entries or to impose data size limitations.
+
+=cut
+
+sub size {
+    my $self = shift;
+
+    my $size = 0;
+
+    # tags
+    $size += length(encode('UTF-8', $self->tags // ''));
+
+    # attributes (strings)
+    while (my ($key, $string) = each %{$self->strings}) {
+        next if !defined $string->{value};
+        $size += length(encode('UTF-8', $key)) + length(encode('UTF-8', $string->{value} // ''));
+    }
+
+    # custom data
+    while (my ($key, $item) = each %{$self->custom_data}) {
+        next if !defined $item->{value};
+        $size += length(encode('UTF-8', $key)) + length(encode('UTF-8', $item->{value} // ''));
+    }
+
+    # binaries
+    while (my ($key, $binary) = each %{$self->binaries}) {
+        next if !defined $binary->{value};
+        my $value_len = utf8::is_utf8($binary->{value}) ? length(encode('UTF-8', $binary->{value}))
+            : length($binary->{value});
+        $size += length(encode('UTF-8', $key)) + $value_len;
+    }
+
+    # autotype associations
+    for my $association (@{$self->auto_type->{associations} || []}) {
+        $size += length(encode('UTF-8', $association->{window}))
+            + length(encode('UTF-8', $association->{keystroke_sequence} // ''));
+    }
+
+    return $size;
+}
+
+##############################################################################
+
+sub history {
+    my $self = shift;
+    return [map { __PACKAGE__->wrap($_, $self->kdbx) } @{$self->{history} || []}];
+}
+
+=method history_size
+
+    $size = $entry->history_size;
+
+Get the size (in bytes) of all historical entries combined.
+
+=cut
+
+sub history_size {
+    my $self = shift;
+    return sum0 map { $_->size } @{$self->history};
+}
+
+=method prune_history
+
+    $entry->prune_history(%options);
+
+Remove as many older historical entries as necessary to get under the database limits. The limits are taken
+from the database or can be specified with C<%options>:
+
+=for :list
+* C<max_items> - Maximum number of historical entries to keep (default: 10, no limit: -1)
+* C<max_size> - Maximum total size (in bytes) of historical entries to keep (default: 6 MiB, no limit: -1)
+
+=cut
+
+sub prune_history {
+    my $self = shift;
+    my %args = @_;
+
+    my $max_items = $args{max_items} // eval { $self->kdbx->history_max_items }
+        // HISTORY_DEFAULT_MAX_ITEMS;
+    my $max_size  = $args{max_size} // eval { $self->kdbx->history_max_size }
+        // HISTORY_DEFAULT_MAX_SIZE;
+
+    # history is ordered oldest to youngest
+    my $history = $self->history;
+
+    if (0 <= $max_items && $max_items < @$history) {
+        splice @$history, -$max_items;
+    }
+
+    if (0 <= $max_size) {
+        my $current_size = $self->history_size;
+        while ($max_size < $current_size) {
+            my $entry = shift @$history;
+            $current_size -= $entry->size;
+        }
+    }
+}
+
+sub add_history {
+    my $self = shift;
+    delete $_->{history} for @_;
+    push @{$self->{history} //= []}, @_;
+}
+
+##############################################################################
+
+sub begin_work {
+    my $self = shift;
+    require File::KDBX::Transaction;
+    return File::KDBX::Transaction->new($self, @_);
+}
+
+sub _commit {
+    my $self = shift;
+    my $txn  = shift;
+    $self->add_history($txn->original);
+    $self->last_modification_time(gmtime);
+}
+
+sub TO_JSON { +{%{$_[0]}} }
+
+1;
+__END__
+
+=head1 DESCRIPTION
+
+An entry in a KDBX database is a record that can contains strings (also called "fields") and binaries (also
+called "files" or "attachments"). Every string and binary has a key or name. There is a default set of strings
+that every entry has:
+
+=for :list
+* C<Title>
+* C<UserName>
+* C<Password>
+* C<URL>
+* C<Notes>
+
+Beyond this, you can store any number of other strings and any number of binaries that you can use for
+whatever purpose you want.
+
+There is also some metadata associated with an entry. Each entry in a database is identified uniquely by
+a UUID. An entry can also have an icon associated with it, and there are various timestamps. Take a look at
+the attributes to see what's available.
+
+=head2 Placeholders
+
+Entry strings and auto-type key sequences can have placeholders or template tags that can be replaced by other
+values. Placeholders can appear like C<{PLACEHOLDER}>. For example, a B<URL> string might have a value of
+C<http://example.com?user={USERNAME}>. C<{USERNAME}> is a placeholder for the value of the B<UserName> string
+of the same entry. If the C<UserName> string had a value of "batman", the B<URL> string would expand to
+C<http://example.com?user=batman>.
+
+Some placeholders take an argument, where the argument follows the tag after a colon. The syntax for this is
+C<{PLACEHOLDER:ARGUMENT}>.
+
+Placeholders are documented in the L<KeePass Help Center|https://keepass.info/help/base/placeholders.html>.
+This software supports many (but not all) of the placeholders documented there.
+
+=head3 Entry Placeholders
+
+=for :list
+* ☑ C<{TITLE}> - B<Title> string
+* ☑ C<{USERNAME}> - B<UserName> string
+* ☑ C<{PASSWORD}> - B<Password> string
+* ☑ C<{NOTES}> - B<Notes> string
+* ☑ C<{URL}> - B<URL> string
+* ☑ C<{URL:SCM}> / C<{URL:SCHEME}>
+* ☑ C<{URL:USERINFO}>
+* ☑ C<{URL:USERNAME}>
+* ☑ C<{URL:PASSWORD}>
+* ☑ C<{URL:HOST}>
+* ☑ C<{URL:PORT}>
+* ☑ C<{URL:PATH}>
+* ☑ C<{URL:QUERY}>
+* ☑ C<{URL:FRAGMENT}> / C<{URL:HASH}>
+* ☑ C<{URL:RMVSCM}> / C<{URL:WITHOUTSCHEME}>
+* ☑ C<{S:Name}> - Custom string where C<Name> is the name or key of the string
+* ☑ C<{UUID}> - Identifier (32 hexidecimal characters)
+* ☑ C<{HMACOTP}> - Generate an HMAC-based one-time password
+* ☑ C<{TIMEOTP}> - Generate a time-based one-time password
+* ☑ C<{GROUP_NOTES}> - Notes of the parent group
+* ☑ C<{GROUP_PATH}> - Full path of the parent group
+* ☑ C<{GROUP}> - Name of the parent group
+
+=head3 Field References
+
+=for :list
+* ☑ C<{REF:Wanted@SearchIn:Text}> - See L<File::KDBX/resolve_reference>
+
+=head3 File path Placeholders
+
+=for :list
+* ☑ C<{APPDIR}> - Program directory path
+* ☑ C<{FIREFOX}> - Path to the Firefox browser executable
+* ☑ C<{GOOGLECHROME}> - Path to the Chrome browser executable
+* ☑ C<{INTERNETEXPLORER}> - Path to the Firefox browser executable
+* ☑ C<{OPERA}> - Path to the Opera browser executable
+* ☑ C<{SAFARI}> - Path to the Safari browser executable
+* ☒ C<{DB_PATH}> - Full file path of the database
+* ☒ C<{DB_DIR}> - Directory path of the database
+* ☒ C<{DB_NAME}> - File name (including extension) of the database
+* ☒ C<{DB_BASENAME}> - File name (excluding extension) of the database
+* ☒ C<{DB_EXT}> - File name extension
+* ☑ C<{ENV_DIRSEP}> - Directory separator
+* ☑ C<{ENV_PROGRAMFILES_X86}> - One of C<%ProgramFiles(x86)%> or C<%ProgramFiles%>
+
+=head3 Date and Time Placeholders
+
+=for :list
+* ☑ C<{DT_SIMPLE}> - Current local date and time as a sortable string
+* ☑ C<{DT_YEAR}> - Year component of the current local date
+* ☑ C<{DT_MONTH}> - Month component of the current local date
+* ☑ C<{DT_DAY}> - Day component of the current local date
+* ☑ C<{DT_HOUR}> - Hour component of the current local time
+* ☑ C<{DT_MINUTE}> - Minute component of the current local time
+* ☑ C<{DT_SECOND}> - Second component of the current local time
+* ☑ C<{DT_UTC_SIMPLE}> - Current UTC date and time as a sortable string
+* ☑ C<{DT_UTC_YEAR}> - Year component of the current UTC date
+* ☑ C<{DT_UTC_MONTH}> - Month component of the current UTC date
+* ☑ C<{DT_UTC_DAY}> - Day component of the current UTC date
+* ☑ C<{DT_UTC_HOUR}> - Hour component of the current UTC time
+* ☑ C<{DT_UTC_MINUTE}> Minute Year component of the current UTC time
+* ☑ C<{DT_UTC_SECOND}> - Second component of the current UTC time
+
+If the current date and time is <2012-07-25 17:05:34>, the "simple" form would be C<20120725170534>.
+
+=head3 Special Key Placeholders
+
+Certain placeholders for use in auto-type key sequences are not supported for replacement, but they will
+remain as-is so that an auto-type engine (not included) can parse and replace them with the appropriate
+virtual key presses. For completeness, here is the list that the KeePass program claims to support:
+
+C<{TAB}>, C<{ENTER}>, C<{UP}>, C<{DOWN}>, C<{LEFT}>, C<{RIGHT}>, C<{HOME}>, C<{END}>, C<{PGUP}>, C<{PGDN}>,
+C<{INSERT}>, C<{DELETE}>, C<{SPACE}>
+
+C<{BACKSPACE}>, C<{BREAK}>, C<{CAPSLOCK}>, C<{ESC}>, C<{WIN}>, C<{LWIN}>, C<{RWIN}>, C<{APPS}>, C<{HELP}>,
+C<{NUMLOCK}>, C<{PRTSC}>, C<{SCROLLLOCK}>
+
+C<{F1}>, C<{F2}>, C<{F3}>, C<{F4}>, C<{F5}>, C<{F6}>, C<{F7}>, C<{F8}>, C<{F9}>, C<{F10}>, C<{F11}>, C<{F12}>,
+C<{F13}>, C<{F14}>, C<{F15}>, C<{F16}>
+
+C<{ADD}>, C<{SUBTRACT}>, C<{MULTIPLY}>, C<{DIVIDE}>, C<{NUMPAD0}>, C<{NUMPAD1}>, C<{NUMPAD2}>, C<{NUMPAD3}>,
+C<{NUMPAD4}>, C<{NUMPAD5}>, C<{NUMPAD6}>, C<{NUMPAD7}>, C<{NUMPAD8}>, C<{NUMPAD9}>
+
+=head3 Miscellaneous Placeholders
+
+=for :list
+* ☒ C<{BASE}>
+* ☒ C<{BASE:SCM}> / C<{BASE:SCHEME}>
+* ☒ C<{BASE:USERINFO}>
+* ☒ C<{BASE:USERNAME}>
+* ☒ C<{BASE:PASSWORD}>
+* ☒ C<{BASE:HOST}>
+* ☒ C<{BASE:PORT}>
+* ☒ C<{BASE:PATH}>
+* ☒ C<{BASE:QUERY}>
+* ☒ C<{BASE:FRAGMENT}> / C<{BASE:HASH}>
+* ☒ C<{BASE:RMVSCM}> / C<{BASE:WITHOUTSCHEME}>
+* ☒ C<{CLIPBOARD-SET:/Text/}>
+* ☒ C<{CLIPBOARD}>
+* ☒ C<{CMD:/CommandLine/Options/}>
+* ☑ C<{C:Comment}> - Comments are simply replaced by nothing
+* ☑ C<{ENV:} and C<%ENV%> - Environment variables
+* ☒ C<{GROUP_SEL_NOTES}>
+* ☒ C<{GROUP_SEL_PATH}>
+* ☒ C<{GROUP_SEL}>
+* ☒ C<{NEWPASSWORD}>
+* ☒ C<{NEWPASSWORD:/Profile/}>
+* ☒ C<{PASSWORD_ENC}>
+* ☒ C<{PICKCHARS}>
+* ☒ C<{PICKCHARS:Field:Options}>
+* ☒ C<{PICKFIELD}>
+* ☒ C<{T-CONV:/Text/Type/}>
+* ☒ C<{T-REPLACE-RX:/Text/Type/Replace/}>
+
+Some of these that remain unimplemented, such as C<{CLIPBOARD}>, cannot be implemented portably. Some of these
+I haven't implemented (yet) just because they don't seem very useful. You can create your own placeholder to
+augment the list of default supported placeholders or to replace a built-in placeholder handler. To create
+a placeholder, just set it in the C<%File::KDBX::PLACEHOLDERS> hash. For example:
+
+    $File::KDBX::PLACEHOLDERS{'MY_PLACEHOLDER'} = sub {
+        my ($entry) = @_;
+        ...;
+    };
+
+If the placeholder is expanded in the context of an entry, C<$entry> is the B<File::KDBX::Entry> object in
+context. Otherwise it is C<undef>. An entry is in context if, for example, the placeholder is in an entry's
+strings or auto-complete key sequences.
+
+    $File::KDBX::PLACEHOLDERS{'MY_PLACEHOLDER:'} = sub {
+        my ($entry, $arg) = @_;         #    ^ Notice the colon here
+        ...;
+    };
+
+If the name of the placeholder ends in a colon, then it is expected to receive an argument. During expansion,
+everything after the colon and before the end of the placeholder is passed to your placeholder handler
+subroutine. So if the placeholder is C<{MY_PLACEHOLDER:whatever}>, C<$arg> will have the value C<whatever>.
+
+An argument is required for placeholders than take one. I.e. The placeholder handler won't be called if there
+is no argument. If you want a placeholder to support an optional argument, you'll need to set the placeholder
+both with and without a colon (or they could be different subroutines):
+
+    $File::KDBX::PLACEHOLDERS{'RAND'} = $File::KDBX::PLACEHOLDERS{'RAND:'} = sub {
+        (undef, my $arg) = @_;
+        return defined $arg ? rand($arg) : rand;
+    };
+
+You can also remove placeholder handlers. If you want to disable placeholder expansion entirely, just delete
+all the handlers:
+
+    %File::KDBX::PLACEHOLDERS = ();
+
+=head2 One-time Passwords
+
+An entry can be configured to generate one-time passwords, both HOTP (HMAC-based) and TOTP (time-based). The
+configuration storage isn't completely standardized, but this module supports two predominant configuration
+styles:
+
+=for :list
+* L<KeePass 2|https://keepass.info/help/base/placeholders.html#otp>
+* KeePassXC
+
+B<NOTE:> To use this feature, you must install the suggested dependency:
+
+=for :list
+* L<Pass::OTP>
+
+To configure TOTP in the KeePassXC style, there is only one string to set: C<otp>. The value should be any
+valid otpauth URI. When generating an OTP, all of the relevant OTP properties are parsed from the URI.
+
+To configure TOTP in the KeePass 2 style, set the following strings:
+
+=for :list
+* C<TimeOtp-Algorithm> - Cryptographic algorithm, one of C<HMAC-SHA-1> (default), C<HMAC-SHA-256> and
+    C<HMAC-SHA-512>
+* C<TimeOtp-Length> - Number of digits each one-time password is (default: 6, maximum: 8)
+* C<TimeOtp-Period> - Time-step size in seconds (default: 30)
+* C<TimeOtp-Secret> - Text string secret, OR
+* C<TimeOtp-Secret-Hex> - Hexidecimal-encoded secret, OR
+* C<TimeOtp-Secret-Base32> - Base32-encoded secret (most common), OR
+* C<TimeOtp-Secret-Base64> - Base64-encoded secret
+
+To configure HOTP in the KeePass 2 style, set the following strings:
+
+=for :list
+* C<HmacOtp-Counter> - Counting value in decimal, starts on C<0> by default and increments when L</hmac_otp>
+    is called
+* C<HmacOtp-Secret> - Text string secret, OR
+* C<HmacOtp-Secret-Hex> - Hexidecimal-encoded secret, OR
+* C<HmacOtp-Secret-Base32> - Base32-encoded secret (most common), OR
+* C<HmacOtp-Secret-Base64> - Base64-encoded secret
+
+B<NOTE:> The multiple "Secret" strings are simply a way to store a secret in different formats. Only one of
+these should actually be set or an error will be thrown.
+
+Here's a basic example:
+
+    $entry->string(otp => 'otpauth://totp/Issuer:user?secret=NBSWY3DP&issuer=Issuer');
+    # OR
+    $entry->string('TimeOtp-Secret-Base32' => 'NBSWY3DP');
+
+    my $otp = $entry->time_otp;
+
+=cut
This page took 0.037887 seconds and 4 git commands to generate.