File-KDBX

 view release on metacpan or  search on metacpan

lib/File/KDBX/Key/File.pm  view on Meta::CPAN



sub save {
    my $self = shift;
    my %args = @_;

    my @cleanup;
    my $raw_key = $args{raw_key} // $self->raw_key // random_bytes(32);
    push @cleanup, erase_scoped $raw_key;
    length($raw_key) == 32 or throw 'Raw key must be exactly 256 bits (32 bytes)', length => length($raw_key);

    my $type        = $args{type} // $self->type // KEY_FILE_TYPE_XML;
    my $version     = $args{version} // $self->version // 2;
    my $filepath    = $args{filepath} // $self->filepath;
    my $fh          = $args{fh};
    my $atomic      = $args{atomic} // 1;

    my $filepath_temp;
    if (!openhandle($fh)) {
        $filepath or throw 'Must specify where to safe the key file to';

        if ($atomic) {
            require File::Temp;
            ($fh, $filepath_temp) = eval { File::Temp::tempfile("${filepath}-XXXXXX", UNLINK => 1) };
            if (!$fh or my $err = $@) {
                $err //= 'Unknown error';
                throw sprintf('Open file failed (%s): %s', $filepath_temp, $err),
                    error       => $err,
                    filepath    => $filepath_temp;
            }
        }
        else {
            open($fh, '>:raw', $filepath) or throw "Open file failed ($filepath): $!", filepath => $filepath;
        }
    }

    if ($type == KEY_FILE_TYPE_XML) {
        $self->_save_xml($fh, $raw_key, $version);
    }
    elsif ($type == KEY_FILE_TYPE_BINARY) {
        print $fh $raw_key;
    }
    elsif ($type == KEY_FILE_TYPE_HEX) {
        my $hex = uc(unpack('H*', $raw_key));
        push @cleanup, erase_scoped $hex;
        print $fh $hex;
    }
    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;
    }
}

##############################################################################

sub _load_xml {
    my $self = shift;
    my $buf  = shift;
    my $out  = shift;

    my ($version, $hash, $data);

    my $reader  = XML::LibXML::Reader->new(string => $$buf);
    my $pattern = XML::LibXML::Pattern->new('/KeyFile/Meta/Version|/KeyFile/Key/Data');

    while ($reader->nextPatternMatch($pattern) == 1) {
        next if $reader->nodeType != XML_READER_TYPE_ELEMENT;
        my $name = $reader->localName;
        if ($name eq 'Version') {
            $reader->read if !$reader->isEmptyElement;
            $reader->nodeType == XML_READER_TYPE_TEXT
                or alert 'Expected text node with version', line => $reader->lineNumber;
            my $val = trim($reader->value);
            defined $version
                and alert 'Overwriting version', previous => $version, new => $val, line => $reader->lineNumber;
            $version = $val;
        }
        elsif ($name eq 'Data') {
            $hash = trim($reader->getAttribute('Hash')) if $reader->hasAttributes;
            $reader->read if !$reader->isEmptyElement;
            $reader->nodeType == XML_READER_TYPE_TEXT
                or alert 'Expected text node with data', line => $reader->lineNumber;
            $data = $reader->value;
            $data =~ s/\s+//g if defined $data;
        }
    }

    return if !defined $version || !defined $data;

    if ($version =~ /^1\.0/ && $data =~ /^[A-Za-z0-9+\/=]+$/) {
        $$out = eval { decode_b64($data) };
        if (my $err = $@) {
            throw 'Failed to decode key in key file', version => $version, data => $data, error => $err;
        }
        return (KEY_FILE_TYPE_XML, $version);
    }
    elsif ($version =~ /^2\.0/ && $data =~ /^[A-Fa-f0-9]+$/ && defined $hash && $hash =~ /^[A-Fa-f0-9]+$/) {
        $$out = pack('H*', $data);
        $hash = pack('H*', $hash);
        my $got_hash = digest_data('SHA256', $$out);
        $hash eq substr($got_hash, 0, length($hash))
            or throw 'Checksum mismatch', got => $got_hash, expected => $hash;
        return (KEY_FILE_TYPE_XML, $version);
    }

    throw 'Unexpected data in key file', version => $version, data => $data;
}



( run in 0.644 second using v1.01-cache-2.11-cpan-71847e10f99 )