package File::KDBX;
# ABSTRACT: Encrypted database to store secret text and files
+use 5.010;
use warnings;
use strict;
use List::Util qw(any first);
use Ref::Util qw(is_ref is_arrayref is_plain_hashref);
use Scalar::Util qw(blessed);
-use Time::Piece;
+use Time::Piece 1.33;
use boolean;
use namespace::clean;
# copy constructor
return $_[0]->clone if @_ == 1 && blessed $_[0] && $_[0]->isa($class);
- my $self = bless {}, $class;
+ my $data;
+ $data = shift if is_plain_hashref($_[0]);
+
+ my $self = bless $data // {}, $class;
$self->init(@_);
- $self->_set_nonlazy_attributes if empty $self;
+ $self->_set_nonlazy_attributes if !$data;
return $self;
}
# HEADERS
has 'headers.comment' => '', coerce => \&to_string;
-has 'headers.cipher_id' => CIPHER_UUID_CHACHA20, coerce => \&to_uuid;
+has 'headers.cipher_id' => sub { $_[0]->version < KDBX_VERSION_4_0 ? CIPHER_UUID_AES256 : CIPHER_UUID_CHACHA20 },
+ coerce => \&to_uuid;
has 'headers.compression_flags' => COMPRESSION_GZIP, coerce => \&to_compression_constant;
has 'headers.master_seed' => sub { random_bytes(32) }, coerce => \&to_string;
-has 'headers.encryption_iv' => sub { random_bytes(16) }, coerce => \&to_string;
+has 'headers.encryption_iv' => sub { random_bytes($_[0]->version < KDBX_VERSION_4_0 ? 16 : 12) },
+ coerce => \&to_string;
has 'headers.stream_start_bytes' => sub { random_bytes(32) }, coerce => \&to_string;
has 'headers.kdf_parameters' => sub {
+{
least C<KDBX_VERSION_4_0> (i.e. C<0x00040000>) because Argon2 was introduced with KDBX4.
This method never returns less than C<KDBX_VERSION_3_1> (i.e. C<0x00030001>). That file version is so
-ubiquitious and well-supported, there are seldom reasons to dump in a lesser format nowadays.
+ubiquitous and well-supported, there are seldom reasons to dump in a lesser format nowadays.
B<WARNING:> If you dump a database with a minimum version higher than the current L</version>, the dumper will
typically issue a warning and automatically upgrade the database. This seems like the safest behavior in order
my %args = @_ % 2 == 0 ? @_ : (base => shift, @_);
my $base = delete $args{base} // $self->root;
- return $base->groups_deeply(%args);
+ return $base->all_groups(%args);
}
##############################################################################
$kdbx->add_entry($entry, %options);
$kdbx->add_entry(%entry_attributes, %options);
-Add a entry to a database. This is equivalent to identifying a parent group and calling
+Add an entry to a database. This is equivalent to identifying a parent group and calling
L<File::KDBX::Group/add_entry> on the parent group, forwarding the arguments. Available options:
=for :list
my %args = @_ % 2 == 0 ? @_ : (base => shift, @_);
my $base = delete $args{base} // $self->root;
- return $base->entries_deeply(%args);
+ return $base->all_entries(%args);
}
##############################################################################
my %args = @_ % 2 == 0 ? @_ : (base => shift, @_);
my $base = delete $args{base} // $self->root;
- return $base->objects_deeply(%args);
+ return $base->all_objects(%args);
}
sub __iter__ { $_[0]->objects }
sub lock {
my $self = shift;
- $self->_safe and return $self;
-
+ # Find things to lock:
my @strings;
-
$self->entries(history => 1)->each(sub {
- push @strings, grep { $_->{protect} } values %{$_->strings}, values %{$_->binaries};
+ my $strings = $_->strings;
+ for my $string_key (keys %$strings) {
+ my $string = $strings->{$string_key};
+ push @strings, $string if $string->{protect} // $self->memory_protection($string_key);
+ }
+ push @strings, grep { $_->{protect} } values %{$_->binaries};
});
+ return $self if !@strings; # nothing to do
- $self->_safe(File::KDBX::Safe->new(\@strings));
-
+ if (my $safe = $self->_safe) {
+ $safe->add(\@strings);
+ }
+ else {
+ $self->_safe(File::KDBX::Safe->new(\@strings));
+ }
return $self;
}
limit: -1)
* C<max_size> - Maximum total size (in bytes) of historical entries to keep (default: value of
L</history_max_size>, no limit: -1)
-* C<max_age> - Maximum age (in days) of historical entries to keep (default: 365, no limit: -1)
+* C<max_age> - Maximum age (in days) of historical entries to keep (default: value of
+ L</maintenance_history_days>, no limit: -1)
=cut
* L</transform_seed>
Randomizing these values has no effect on a loaded database. These are only used when a database is dumped.
-You normally do not need to call this method explicitly because the dumper does it explicitly by default.
+You normally do not need to call this method explicitly because the dumper does it for you by default.
=cut
sub randomize_seeds {
my $self = shift;
- $self->encryption_iv(random_bytes(16));
+ my $iv_size = 16;
+ $iv_size = $self->cipher(key => "\0" x 32)->iv_size if KDBX_VERSION_4_0 <= $self->version;
+ $self->encryption_iv(random_bytes($iv_size));
$self->inner_random_stream_key(random_bytes(64));
$self->master_seed(random_bytes(32));
$self->stream_start_bytes(random_bytes(32));
my %args = @_ % 2 == 1 ? (params => shift, @_) : @_;
my $params = $args{params};
- my $compat = $args{compatible} // 1;
$params //= $self->kdf_parameters;
$params = {%{$params || {}}};
sub transform_seed {
my $self = shift;
+ my $param = KDF_PARAM_AES_SEED; # Short cut: Argon2 uses the same parameter name ("S")
$self->headers->{+HEADER_TRANSFORM_SEED} =
- $self->headers->{+HEADER_KDF_PARAMETERS}{+KDF_PARAM_AES_SEED} = shift if @_;
+ $self->headers->{+HEADER_KDF_PARAMETERS}{$param} = shift if @_;
$self->headers->{+HEADER_TRANSFORM_SEED} =
- $self->headers->{+HEADER_KDF_PARAMETERS}{+KDF_PARAM_AES_SEED} //= random_bytes(32);
+ $self->headers->{+HEADER_KDF_PARAMETERS}{$param} //= random_bytes(32);
}
sub transform_rounds {
my $self = shift;
+ require File::KDBX::KDF;
+ my $info = $File::KDBX::KDF::ROUNDS_INFO{$self->kdf_parameters->{+KDF_PARAM_UUID} // ''} //
+ $File::KDBX::KDF::DEFAULT_ROUNDS_INFO;
$self->headers->{+HEADER_TRANSFORM_ROUNDS} =
- $self->headers->{+HEADER_KDF_PARAMETERS}{+KDF_PARAM_AES_ROUNDS} = shift if @_;
+ $self->headers->{+HEADER_KDF_PARAMETERS}{$info->{p}} = shift if @_;
$self->headers->{+HEADER_TRANSFORM_ROUNDS} =
- $self->headers->{+HEADER_KDF_PARAMETERS}{+KDF_PARAM_AES_ROUNDS} //= 100_000;
+ $self->headers->{+HEADER_KDF_PARAMETERS}{$info->{p}} //= $info->{d};
}
=method cipher
my $self = shift;
my %args = @_;
- $args{uuid} //= $self->headers->{+HEADER_CIPHER_ID};
- $args{iv} //= $self->headers->{+HEADER_ENCRYPTION_IV};
+ $args{uuid} //= $self->cipher_id;
+ $args{iv} //= $self->encryption_iv;
require File::KDBX::Cipher;
return File::KDBX::Cipher->new(%args);
=attr comment
-A text string associated with the database. Often unset.
+A text string associated with the database stored unencrypted in the file header. Often unset.
=attr cipher_id
=attr transform_rounds
The number of rounds or iterations used in the key derivation function. Increasing this number makes loading
-and saving the database slower by design in order to make dictionary and brute force attacks more costly.
+and saving the database slower in order to make dictionary and brute force attacks more costly.
=attr encryption_iv
use File::KDBX;
+ # Create a new database from scratch
my $kdbx = File::KDBX->new;
+ # Add some objects to the database
my $group = $kdbx->add_group(
name => 'Passwords',
);
-
my $entry = $group->add_entry(
title => 'My Bank',
+ username => 'mreynolds',
password => 's3cr3t',
);
- $kdbx->dump_file('passwords.kdbx', 'M@st3rP@ssw0rd!');
+ # Save the database to the filesystem
+ $kdbx->dump_file('passwords.kdbx', 'masterpw changeme');
- $kdbx = File::KDBX->load_file('passwords.kdbx', 'M@st3rP@ssw0rd!');
+ # Load the database from the filesystem into a new database instance
+ my $kdbx2 = File::KDBX->load_file('passwords.kdbx', 'masterpw changeme');
- $kdbx->entries->each(sub {
- my ($entry) = @_;
+ # Iterate over database entries, print entry titles
+ $kdbx2->entries->each(sub($entry, @) {
say 'Entry: ', $entry->title;
});
my $kdbx = File::KDBX->load_file('mypasswords.kdbx', 'master password CHANGEME');
$kdbx->unlock; # cause $entry->password below to be defined
- $kdbx->entries->each(sub {
- my ($entry) = @_;
+ $kdbx->entries->each(sub($entry, @) {
say 'Found password for: ', $entry->title;
say ' Username: ', $entry->username;
say ' Password: ', $entry->password;
generate strong keys.
The KDBX format allows for the key derivation function to be tuned. The idea is that you want each single
-brute-foce attempt to be expensive (in terms of time, CPU usage or memory usage), so that making a lot of
+brute-force attempt to be expensive (in terms of time, CPU usage or memory usage), so that making a lot of
attempts (which would be required if you have a strong master key) gets I<really> expensive.
How expensive you want to make each attempt is up to you and can depend on the application.
It helps to read it right-to-left, like "usage_count is greater than or equal to 5".
-If you find the disambiguating structures to be distracting or confusing, you can also the
+If you find the disambiguating structures to be distracting or confusing, you can also use the
L<File::KDBX::Util/simple_expression_query> function as a more intuitive alternative. The following example is
equivalent to the previous:
Note: L<File::KDBX::Constants/ICON_SMARTPHONE> is just a constant from L<File::KDBX::Constants>. It isn't
special to this example or to queries generally. We could have just used a literal number.
-The important thing to notice here is how we wrapped the condition in another arrayref with a single key-value
+The important thing to notice here is how we wrapped the condition in another hashref with a single key-value
pair where the key is the name of an operator and the value is the thing to match against. The supported
operators are:
* C<PERL_ONLY> - Do not use L<File::KDBX::XS> if true (default: false)
* C<NO_FORK> - Do not fork if true (default: false)
-=head1 CAVEATS
-
-Some features (e.g. parsing) require 64-bit perl. It should be possible and actually pretty easy to make it
-work using L<Math::BigInt>, but I need to build a 32-bit perl in order to test it and frankly I'm still
-figuring out how. I'm sure it's simple so I'll mark this one "TODO", but for now an exception will be thrown
-when trying to use such features with undersized IVs.
-
=head1 SEE ALSO
=for :list