]> Dogcows Code - chaz/p5-File-KDBX/blob - t/kdbx4.t
5fee0868d52b195aefc18cfcb3025fc833a901cf
[chaz/p5-File-KDBX] / t / kdbx4.t
1 #!/usr/bin/env perl
2
3 use utf8;
4 use warnings;
5 use strict;
6
7 use lib 't/lib';
8 use TestCommon;
9
10 use File::KDBX;
11 use File::KDBX::Constants qw(:version :kdf);
12 use Test::Deep;
13 use Test::More;
14 use boolean qw(:all);
15
16 subtest 'Verify Format400' => sub {
17 my $kdbx = File::KDBX->load(testfile('Format400.kdbx'), 't');
18 $kdbx->unlock;
19
20 ok_magic $kdbx, KDBX_VERSION_4_0, 'Get the correct KDBX4 file magic';
21
22 cmp_deeply $kdbx->headers, {
23 cipher_id => "\326\3\212+\213oL\265\245\$3\2321\333\265\232",
24 compression_flags => 1,
25 encryption_iv => "3?\207P\233or\220\215h\2240",
26 kdf_parameters => {
27 "\$UUID" => "\357cm\337\214)DK\221\367\251\244\3\343\n\f",
28 I => 2,
29 M => 1048576,
30 P => 2,
31 S => "V\254\6m-\206*\260\305\f\0\366\24:4\235\364A\362\346\221\13)}\250\217P\303\303\2\331\245",
32 V => 19,
33 },
34 master_seed => ";\372y\300yS%\3331\177\231\364u\265Y\361\225\3273h\332R,\22\240a\240\302\271\357\313\23",
35 }, 'Extract headers' or diag explain $kdbx->headers;
36
37 is $kdbx->meta->{database_name}, 'Format400', 'Extract database name from meta';
38 is $kdbx->root->name, 'Format400', 'Extract name of root group';
39
40 my ($entry, @other) = $kdbx->find_entries([\'400', 'title']);
41 is @other, 0, 'Database has one entry';
42
43 is $entry->title, 'Format400', 'Entry is titled';
44 is $entry->username, 'Format400', 'Entry has a username set';
45 is keys %{$entry->strings}, 6, 'Entry has six strings';
46 is $entry->string_value('Format400'), 'Format400', 'Entry has a custom string';
47 is keys %{$entry->binaries}, 1, 'Entry has one binary';
48 is $entry->binary_value('Format400'), "Format400\n", 'Entry has a binary string';
49 };
50
51 subtest 'KDBX4 upgrade' => sub {
52 my $kdbx = File::KDBX->new;
53
54 $kdbx->kdf_parameters->{+KDF_PARAM_UUID} = KDF_UUID_AES_CHALLENGE_RESPONSE;
55 is $kdbx->minimum_version, KDBX_VERSION_4_0, 'AES challenge-response KDF requires upgrade';
56 $kdbx->kdf_parameters->{+KDF_PARAM_UUID} = KDF_UUID_ARGON2D;
57 is $kdbx->minimum_version, KDBX_VERSION_4_0, 'Argon2D KDF requires upgrade';
58 $kdbx->kdf_parameters->{+KDF_PARAM_UUID} = KDF_UUID_ARGON2ID;
59 is $kdbx->minimum_version, KDBX_VERSION_4_0, 'Argon2ID KDF requires upgrade';
60 $kdbx->kdf_parameters->{+KDF_PARAM_UUID} = KDF_UUID_AES;
61 is $kdbx->minimum_version, KDBX_VERSION_3_1, 'Reset upgrade requirement';
62
63 $kdbx->public_custom_data->{foo} = 42;
64 is $kdbx->minimum_version, KDBX_VERSION_4_0, 'Public custom data requires upgrade';
65 delete $kdbx->public_custom_data->{foo};
66 is $kdbx->minimum_version, KDBX_VERSION_3_1, 'Reset upgrade requirement';
67
68 my $entry = $kdbx->add_entry;
69 $entry->custom_data(foo => 'bar');
70 is $kdbx->minimum_version, KDBX_VERSION_4_0, 'Entry custom data requires upgrade';
71 delete $entry->custom_data->{foo};
72 is $kdbx->minimum_version, KDBX_VERSION_3_1, 'Reset upgrade requirement';
73
74 my $group = $kdbx->add_group;
75 $group->custom_data(foo => 'bar');
76 is $kdbx->minimum_version, KDBX_VERSION_4_0, 'Group custom data requires upgrade';
77 delete $group->custom_data->{foo};
78 is $kdbx->minimum_version, KDBX_VERSION_3_1, 'Reset upgrade requirement';
79 };
80
81 subtest 'KDBX4.1 upgrade' => sub {
82 my $kdbx = File::KDBX->new;
83
84 my $group1 = $kdbx->add_group;
85 my $group2 = $kdbx->add_group;
86 my $entry1 = $kdbx->add_entry;
87
88 $group1->tags('hi');
89 is $kdbx->minimum_version, KDBX_VERSION_4_1, 'Groups with tags requires upgrade';
90 $group1->tags('');
91 is $kdbx->minimum_version, KDBX_VERSION_3_1, 'Reset upgrade requirement';
92
93 $entry1->quality_check(0);
94 is $kdbx->minimum_version, KDBX_VERSION_4_1, 'Disable entry quality check requires upgrade';
95 $entry1->quality_check(1);
96 is $kdbx->minimum_version, KDBX_VERSION_3_1, 'Reset upgrade requirement';
97
98 $group1->previous_parent_group($group2->uuid);
99 is $kdbx->minimum_version, KDBX_VERSION_4_1, 'Previous parent group on group requires upgrade';
100 $group1->previous_parent_group(undef);
101 is $kdbx->minimum_version, KDBX_VERSION_3_1, 'Reset upgrade requirement';
102
103 $entry1->previous_parent_group($group2->uuid);
104 is $kdbx->minimum_version, KDBX_VERSION_4_1, 'Previous parent group on entry requires upgrade';
105 $entry1->previous_parent_group(undef);
106 is $kdbx->minimum_version, KDBX_VERSION_3_1, 'Reset upgrade requirement';
107
108 $kdbx->add_custom_icon('data');
109 is $kdbx->minimum_version, KDBX_VERSION_3_1, 'Icon with no metadata requires no upgrade';
110 my $icon_uuid = $kdbx->add_custom_icon('data2', name => 'icon name');
111 is $kdbx->minimum_version, KDBX_VERSION_4_1, 'Icon with name requires upgrade';
112 $kdbx->remove_custom_icon($icon_uuid);
113 is $kdbx->minimum_version, KDBX_VERSION_3_1, 'Reset upgrade requirement';
114 $icon_uuid = $kdbx->add_custom_icon('data2', last_modification_time => gmtime);
115 is $kdbx->minimum_version, KDBX_VERSION_4_1, 'Icon with modtime requires upgrade';
116 $kdbx->remove_custom_icon($icon_uuid);
117 is $kdbx->minimum_version, KDBX_VERSION_3_1, 'Reset upgrade requirement';
118
119 $entry1->custom_data(foo => 'bar', last_modification_time => scalar gmtime);
120 is $kdbx->minimum_version, KDBX_VERSION_4_1, 'Entry custom data modtime requires upgrade';
121 delete $entry1->custom_data->{foo};
122 is $kdbx->minimum_version, KDBX_VERSION_3_1, 'Reset upgrade requirement';
123
124 $group1->custom_data(foo => 'bar', last_modification_time => scalar gmtime);
125 is $kdbx->minimum_version, KDBX_VERSION_4_1, 'Group custom data modtime requires upgrade';
126 delete $group1->custom_data->{foo};
127 is $kdbx->minimum_version, KDBX_VERSION_3_1, 'Reset upgrade requirement';
128 };
129
130 sub test_upgrade_master_key_integrity {
131 my ($modifier, $expected_version) = @_;
132 plan tests => $expected_version >= KDBX_VERSION_4_0 ? 6 : 5;
133
134 my $kdbx = File::KDBX->new;
135 $kdbx->kdf_parameters(fast_kdf);
136
137 is $kdbx->kdf->uuid, KDF_UUID_AES, 'Default KDF is AES';
138
139 {
140 local $_ = $kdbx;
141 $modifier->($kdbx);
142 }
143 is $kdbx->minimum_version, $expected_version,
144 sprintf('Got expected minimum version after modification: %x', $kdbx->minimum_version);
145
146 my $master_key = ['fffqcvq4rc', \'this is a keyfile', sub { 'chalresp 523rf2' }];
147 my $dump;
148 warnings { $kdbx->dump_string(\$dump, $master_key) };
149 ok $dump, 'Can dump the database' or diag explain $dump;
150
151 like exception { File::KDBX->load_string($dump, 'wrong key') },
152 qr/invalid credentials/i, 'Cannot load a KDBX with the wrong key';
153
154 # print STDERR "DUMP: [$dump]\n";
155
156 my $kdbx2 = File::KDBX->load_string($dump, $master_key);
157
158 is $kdbx2->version, $expected_version, sprintf('Got expected version: %x', $kdbx2->version);
159 isnt $kdbx2->kdf->uuid, KDF_UUID_AES, 'No unexpected KDF' if $kdbx2->version >= KDBX_VERSION_4_0;
160
161 # diag explain(File::KDBX->load_string($dump, $master_key, inner_format => 'Raw')->raw);
162 }
163 for my $test (
164 [KDBX_VERSION_3_1, 'nothing', sub {}],
165 [KDBX_VERSION_3_1, 'AES KDF', sub { $_->kdf_parameters(fast_kdf(KDF_UUID_AES)) }],
166 [KDBX_VERSION_4_0, 'Argon2D KDF', sub { $_->kdf_parameters(fast_kdf(KDF_UUID_ARGON2D)) }],
167 [KDBX_VERSION_4_0, 'Argon2ID KDF', sub { $_->kdf_parameters(fast_kdf(KDF_UUID_ARGON2ID)) }],
168 [KDBX_VERSION_4_0, 'public custom data', sub { $_->public_custom_data->{foo} = 'bar' }],
169 [KDBX_VERSION_3_1, 'custom data', sub { $_->custom_data(foo => 'bar') }],
170 [KDBX_VERSION_4_0, 'root group custom data', sub { $_->root->custom_data(baz => 'qux') }],
171 [KDBX_VERSION_4_0, 'group custom data', sub { $_->add_group->custom_data(baz => 'qux') }],
172 [KDBX_VERSION_4_0, 'entry custom data', sub { $_->add_entry->custom_data(baz => 'qux') }],
173 ) {
174 my ($expected_version, $name, $modifier) = @$test;
175 subtest "Master key integrity: $name" => \&test_upgrade_master_key_integrity,
176 $modifier, $expected_version;
177 }
178
179 subtest 'Custom data' => sub {
180 my $kdbx = File::KDBX->new;
181 $kdbx->kdf_parameters(fast_kdf(KDF_UUID_AES));
182 $kdbx->version(KDBX_VERSION_4_0);
183
184 $kdbx->public_custom_data->{str} = '你好';
185 $kdbx->public_custom_data->{num} = 42;
186 $kdbx->public_custom_data->{bool} = true;
187 $kdbx->public_custom_data->{bytes} = "\1\2\3\4";
188
189 my $group = $kdbx->add_group(label => 'Group');
190 $group->custom_data(str => '你好');
191 $group->custom_data(num => 42);
192 $group->custom_data(bool => true);
193
194 my $entry = $kdbx->add_entry(label => 'Entry');
195 $entry->custom_data(str => '你好');
196 $entry->custom_data(num => 42);
197 $entry->custom_data(bool => false);
198
199 my $dump = $kdbx->dump_string('a');
200 my $kdbx2 = File::KDBX->load_string($dump, 'a');
201
202 is $kdbx2->public_custom_data->{str}, '你好', 'Store a string in public custom data';
203 cmp_ok $kdbx2->public_custom_data->{num}, '==', 42, 'Store a number in public custom data';
204 is $kdbx2->public_custom_data->{bool}, true, 'Store a boolean in public custom data';
205 ok isBoolean($kdbx2->public_custom_data->{bool}), 'Boolean is indeed a boolean';
206 is $kdbx2->public_custom_data->{bytes}, "\1\2\3\4", 'Store some bytes in public custom data';
207
208 my ($group2) = $kdbx2->find_groups({label => 'Group'});
209 is_deeply $group2->custom_data_value('str'), '你好', 'Store a string in group custom data';
210 is_deeply $group2->custom_data_value('num'), '42', 'Store a number in group custom data';
211 is_deeply $group2->custom_data_value('bool'), '1', 'Store a boolean in group custom data';
212
213 my ($entry2) = $kdbx2->find_entries({label => 'Entry'});
214 is_deeply $entry2->custom_data_value('str'), '你好', 'Store a string in entry custom data';
215 is_deeply $entry2->custom_data_value('num'), '42', 'Store a number in entry custom data';
216 is_deeply $entry2->custom_data_value('bool'), '0', 'Store a boolean in entry custom data';
217 };
218
219 done_testing;
This page took 0.05105 seconds and 3 git commands to generate.