From aeec1adc3f3a80967d0cd9ca2f712491e4ee9b9c Mon Sep 17 00:00:00 2001 From: Charles McGarvey Date: Mon, 18 Apr 2022 19:53:14 -0600 Subject: [PATCH] Save key files atomically --- lib/File/KDBX/Key/File.pm | 25 ++++++++++++++++++++++++- t/keys.t | 3 ++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/lib/File/KDBX/Key/File.pm b/lib/File/KDBX/Key/File.pm index 5c7cb12..5949d4c 100644 --- a/lib/File/KDBX/Key/File.pm +++ b/lib/File/KDBX/Key/File.pm @@ -163,9 +163,18 @@ sub save { my $filepath = $args{filepath} // $self->filepath; my $fh = $args{fh}; + my $filepath_temp; if (!openhandle($fh)) { $filepath or throw 'Must specify where to safe the key file to'; - open($fh, '>:raw', $filepath) or throw "Failed to open key file for writing: $!"; + + require File::Temp; + ($fh, $filepath_temp) = eval { File::Temp::tempfile("${filepath}-XXXXXX", CLEANUP => 1) }; + if (!$fh or my $err = $@) { + $err //= 'Unknown error'; + throw sprintf('Open file failed (%s): %s', $filepath_temp, $err), + error => $err, + filepath => $filepath_temp; + } } if ($type == KEY_FILE_TYPE_XML) { @@ -182,6 +191,20 @@ sub save { else { throw "Cannot save $type key file (invalid type)", type => $type; } + + close($fh); + + if ($filepath_temp) { + my ($file_mode, $file_uid, $file_gid) = (stat($filepath))[2, 4, 5]; + + my $mode = $args{mode} // $file_mode // do { my $m = umask; defined $m ? oct(666) &~ $m : undef }; + my $uid = $args{uid} // $file_uid // -1; + my $gid = $args{gid} // $file_gid // -1; + chmod($mode, $filepath_temp) if defined $mode; + chown($uid, $gid, $filepath_temp); + rename($filepath_temp, $filepath) + or throw "Failed to write file ($filepath): $!", filepath => $filepath; + } } ############################################################################## diff --git a/t/keys.t b/t/keys.t index 62d2a1a..65658e5 100644 --- a/t/keys.t +++ b/t/keys.t @@ -89,7 +89,8 @@ subtest 'IO handle key files' => sub { is $key->type, 'hashed', 'file type is detected as hashed'; my ($fh_save, $filepath) = tempfile('keyfile-XXXXXX', TMPDIR => 1, UNLINK => 1, SUFFIX => '.key'); - ok $key->save(fh => $fh_save, type => KEY_FILE_TYPE_XML), 'Save key file using IO handle'; + is exception { $key->save(fh => $fh_save, type => KEY_FILE_TYPE_XML) }, undef, + 'Save key file using IO handle'; close($fh_save); my $key2 = File::KDBX::Key::File->new($filepath); -- 2.43.0