mirror of https://github.com/digint/btrbk
btrbk: add openssl_enc encryption for raw targets; add system_urandom()
Example: Manually create a key: # KEYFILE=/some/secure/place/btrbk.key # dd if=/dev/urandom bs=1 count=32 | od -x -A n | tr -d "[:space:]" > $KEYFILE btrbk.conf: volume /mnt/btr_pool incremental no raw_target_encrypt openssl_enc openssl_ciphername aes-256-cbc openssl_iv_size 16 # NOTE: set to "no" if no IV is needed by the selected cipher openssl_keyfile /some/secure/place/btrbk.key subvolume home target raw ssh://cloud.example.com/backuppull/204/head
parent
251c2fb2a1
commit
de7628ac7c
|
@ -9,6 +9,7 @@ btrbk-current
|
||||||
* Add "--preserve-snapshots" and "--preserve-backups" options.
|
* Add "--preserve-snapshots" and "--preserve-backups" options.
|
||||||
* Change raw backup format (sidecar file instead of uuid in file).
|
* Change raw backup format (sidecar file instead of uuid in file).
|
||||||
* Honor target_preserve for raw targets (delete raw targets).
|
* Honor target_preserve for raw targets (delete raw targets).
|
||||||
|
* Add symmetric encryption for raw targets (close #157).
|
||||||
* Do not run in "perl taint mode" by default: remove "perl -T" in
|
* Do not run in "perl taint mode" by default: remove "perl -T" in
|
||||||
hashbang; hardcode $PATH only if taint mode is enabled.
|
hashbang; hardcode $PATH only if taint mode is enabled.
|
||||||
* Remove "duration" column from transaction_log/transaction_syslog.
|
* Remove "duration" column from transaction_log/transaction_syslog.
|
||||||
|
|
83
btrbk
83
btrbk
|
@ -62,7 +62,7 @@ my $glob_match = qr/[0-9a-zA-Z_@\+\-\.\/\*]+/; # file_match plus '*'
|
||||||
my $uuid_match = qr/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/;
|
my $uuid_match = qr/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/;
|
||||||
my $timestamp_postfix_match = qr/\.(?<YYYY>[0-9]{4})(?<MM>[0-9]{2})(?<DD>[0-9]{2})(T(?<hh>[0-9]{2})(?<mm>[0-9]{2})((?<ss>[0-9]{2})(?<zz>(Z|[+-][0-9]{4})))?)?(_(?<NN>[0-9]+))?/; # matches "YYYYMMDD[Thhmm[ss+0000]][_NN]"
|
my $timestamp_postfix_match = qr/\.(?<YYYY>[0-9]{4})(?<MM>[0-9]{2})(?<DD>[0-9]{2})(T(?<hh>[0-9]{2})(?<mm>[0-9]{2})((?<ss>[0-9]{2})(?<zz>(Z|[+-][0-9]{4})))?)?(_(?<NN>[0-9]+))?/; # matches "YYYYMMDD[Thhmm[ss+0000]][_NN]"
|
||||||
my $raw_postfix_match_DEPRECATED = qr/--(?<received_uuid>$uuid_match)(\@(?<parent_uuid>$uuid_match))?\.btrfs?(\.(?<compress>($compress_format_alt)))?(\.(?<encrypt>gpg))?(\.(?<split>split))?(\.(?<incomplete>part))?/; # matches ".btrfs_<received_uuid>[@<parent_uuid>][.gz|bz2|xz][.gpg][.split][.part]"
|
my $raw_postfix_match_DEPRECATED = qr/--(?<received_uuid>$uuid_match)(\@(?<parent_uuid>$uuid_match))?\.btrfs?(\.(?<compress>($compress_format_alt)))?(\.(?<encrypt>gpg))?(\.(?<split>split))?(\.(?<incomplete>part))?/; # matches ".btrfs_<received_uuid>[@<parent_uuid>][.gz|bz2|xz][.gpg][.split][.part]"
|
||||||
my $raw_postfix_match = qr/\.btrfs(\.($compress_format_alt))?(\.gpg)?/; # matches ".btrfs[.gz|bz2|xz][.gpg]"
|
my $raw_postfix_match = qr/\.btrfs(\.($compress_format_alt))?(\.(gpg|encrypted))?/; # matches ".btrfs[.gz|bz2|xz][.gpg|encrypted]"
|
||||||
|
|
||||||
my $group_match = qr/[a-zA-Z0-9_:-]+/;
|
my $group_match = qr/[a-zA-Z0-9_:-]+/;
|
||||||
my $ssh_cipher_match = qr/[a-z0-9][a-z0-9@.-]+/;
|
my $ssh_cipher_match = qr/[a-z0-9][a-z0-9@.-]+/;
|
||||||
|
@ -105,11 +105,14 @@ my %config_options = (
|
||||||
raw_target_compress => { default => undef, accept => [ "no", (keys %compression) ] },
|
raw_target_compress => { default => undef, accept => [ "no", (keys %compression) ] },
|
||||||
raw_target_compress_level => { default => "default", accept => [ "default" ], accept_numeric => 1 },
|
raw_target_compress_level => { default => "default", accept => [ "default" ], accept_numeric => 1 },
|
||||||
raw_target_compress_threads => { default => "default", accept => [ "default" ], accept_numeric => 1 },
|
raw_target_compress_threads => { default => "default", accept => [ "default" ], accept_numeric => 1 },
|
||||||
raw_target_encrypt => { default => undef, accept => [ "no", "gpg" ] },
|
raw_target_encrypt => { default => undef, accept => [ "no", "gpg", "openssl_enc" ] },
|
||||||
raw_target_block_size => { default => "128K", accept_regexp => qr/^[0-9]+(kB|k|K|KiB|MB|M|MiB)?$/ },
|
raw_target_block_size => { default => "128K", accept_regexp => qr/^[0-9]+(kB|k|K|KiB|MB|M|MiB)?$/ },
|
||||||
raw_target_split => { default => undef, accept => [ "no" ], accept_regexp => qr/^[0-9]+([kmgtpezyKMGTPEZY][bB]?)?$/ },
|
raw_target_split => { default => undef, accept => [ "no" ], accept_regexp => qr/^[0-9]+([kmgtpezyKMGTPEZY][bB]?)?$/ },
|
||||||
gpg_keyring => { default => undef, accept_file => { absolute => 1 } },
|
gpg_keyring => { default => undef, accept_file => { absolute => 1 } },
|
||||||
gpg_recipient => { default => undef, accept_regexp => qr/^[0-9a-zA-Z_@\+\-\.]+$/ },
|
gpg_recipient => { default => undef, accept_regexp => qr/^[0-9a-zA-Z_@\+\-\.]+$/ },
|
||||||
|
openssl_ciphername => { default => "aes-256-cbc", accept_regexp => qr/^[0-9a-zA-Z\-]+$/ },
|
||||||
|
openssl_iv_size => { default => undef, accept => [ "no", accept_numeric => 1 ] },
|
||||||
|
openssl_keyfile => { default => undef, accept_file => { absolute => 1 } },
|
||||||
|
|
||||||
group => { default => undef, accept_regexp => qr/^$group_match(\s*,\s*$group_match)*$/, split => qr/\s*,\s*/ },
|
group => { default => undef, accept_regexp => qr/^$group_match(\s*,\s*$group_match)*$/, split => qr/\s*,\s*/ },
|
||||||
|
|
||||||
|
@ -231,6 +234,8 @@ my %raw_info_sort = (
|
||||||
compress => 9,
|
compress => 9,
|
||||||
split => 10,
|
split => 10,
|
||||||
encrypt => 11,
|
encrypt => 11,
|
||||||
|
cipher => 12,
|
||||||
|
iv => 13,
|
||||||
);
|
);
|
||||||
|
|
||||||
my %url_cache; # map URL to btr_tree node
|
my %url_cache; # map URL to btr_tree node
|
||||||
|
@ -1379,6 +1384,14 @@ sub btrfs_send_to_file($$$;$$)
|
||||||
$target_filename .= '.' . $compression{$compress->{key}}->{format};
|
$target_filename .= '.' . $compression{$compress->{key}}->{format};
|
||||||
push @cmd_pipe, { compress => $compress }; # does nothing if already compressed by rsh_compress_out
|
push @cmd_pipe, { compress => $compress }; # does nothing if already compressed by rsh_compress_out
|
||||||
}
|
}
|
||||||
|
if($encrypt) {
|
||||||
|
$target_filename .= ($encrypt->{type} eq "gpg") ? '.gpg' : '.encrypted';
|
||||||
|
}
|
||||||
|
|
||||||
|
# NOTE: $ret_vol_received must always be set when function returns!
|
||||||
|
my $vol_received = vinfo_child($target, $target_filename);
|
||||||
|
$$ret_vol_received = $vol_received if(ref $ret_vol_received);
|
||||||
|
|
||||||
if($encrypt) {
|
if($encrypt) {
|
||||||
$raw_info{encrypt} = $encrypt->{type};
|
$raw_info{encrypt} = $encrypt->{type};
|
||||||
|
|
||||||
|
@ -1393,7 +1406,6 @@ sub btrfs_send_to_file($$$;$$)
|
||||||
# generation faster; however sometimes write operations are not
|
# generation faster; however sometimes write operations are not
|
||||||
# desired. This option can be used to achieve that with the cost
|
# desired. This option can be used to achieve that with the cost
|
||||||
# of slower random generation.
|
# of slower random generation.
|
||||||
$target_filename .= '.gpg';
|
|
||||||
my @gpg_options = ( '--batch', '--no-tty', '--no-random-seed-file', '--trust-model', 'always' );
|
my @gpg_options = ( '--batch', '--no-tty', '--no-random-seed-file', '--trust-model', 'always' );
|
||||||
push @gpg_options, ( '--compress-algo', 'none' ) if($compress); # NOTE: if --compress-algo is not set, gpg might still compress according to OpenPGP standard.
|
push @gpg_options, ( '--compress-algo', 'none' ) if($compress); # NOTE: if --compress-algo is not set, gpg might still compress according to OpenPGP standard.
|
||||||
push(@gpg_options, ( '--no-default-keyring', '--keyring', $encrypt->{keyring} )) if($encrypt->{keyring});
|
push(@gpg_options, ( '--no-default-keyring', '--keyring', $encrypt->{keyring} )) if($encrypt->{keyring});
|
||||||
|
@ -1404,6 +1416,35 @@ sub btrfs_send_to_file($$$;$$)
|
||||||
compressed_ok => ($compress ? 1 : 0),
|
compressed_ok => ($compress ? 1 : 0),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
elsif($encrypt->{type} eq "openssl_enc") {
|
||||||
|
# encrypt using "openssl enc"
|
||||||
|
$raw_info{cipher} = $encrypt->{ciphername};
|
||||||
|
|
||||||
|
# NOTE: iv is always generated locally!
|
||||||
|
my $iv_size = $encrypt->{iv_size};
|
||||||
|
my $iv;
|
||||||
|
if($iv_size) {
|
||||||
|
INFO "Generating iv for openssl encryption (cipher=$encrypt->{ciphername})";
|
||||||
|
$iv = system_urandom($iv_size, 'hex');
|
||||||
|
unless($iv) {
|
||||||
|
ERROR "Failed generate IV for openssl_enc: $source->{PRINT}";
|
||||||
|
return undef;
|
||||||
|
}
|
||||||
|
$raw_info{iv} = $iv;
|
||||||
|
}
|
||||||
|
|
||||||
|
my @openssl_options = (
|
||||||
|
'-' . $encrypt->{ciphername},
|
||||||
|
'-K $(cat ' . $encrypt->{keyfile} . ')',
|
||||||
|
);
|
||||||
|
push @openssl_options, ('-iv', $iv) if($iv);
|
||||||
|
|
||||||
|
push @cmd_pipe, {
|
||||||
|
cmd => [ 'openssl', 'enc', '-e', @openssl_options ],
|
||||||
|
name => 'openssl_enc',
|
||||||
|
compressed_ok => ($compress ? 1 : 0),
|
||||||
|
};
|
||||||
|
}
|
||||||
else {
|
else {
|
||||||
die "Usupported encryption type (raw_target_encrypt)";
|
die "Usupported encryption type (raw_target_encrypt)";
|
||||||
}
|
}
|
||||||
|
@ -1442,9 +1483,6 @@ sub btrfs_send_to_file($$$;$$)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
my $vol_received = vinfo_child($target, $target_filename);
|
|
||||||
$$ret_vol_received = $vol_received if(ref $ret_vol_received);
|
|
||||||
|
|
||||||
$raw_info{FILE} = $target_filename;
|
$raw_info{FILE} = $target_filename;
|
||||||
$raw_info{RECEIVED_PARENT_UUID} = $parent_uuid if($parent_uuid);
|
$raw_info{RECEIVED_PARENT_UUID} = $parent_uuid if($parent_uuid);
|
||||||
# disabled for now, as its not very useful and might leak information:
|
# disabled for now, as its not very useful and might leak information:
|
||||||
|
@ -1786,6 +1824,36 @@ sub system_write_raw_info($$)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
sub system_urandom($;$) {
|
||||||
|
my $size = shift;
|
||||||
|
my $format = shift || 'hex';
|
||||||
|
die unless(($size > 0) && ($size <= 256)); # sanity check
|
||||||
|
|
||||||
|
unless(open(URANDOM, '<', '/dev/urandom')) {
|
||||||
|
ERROR "Failed to open /dev/urandom: $!";
|
||||||
|
return undef;
|
||||||
|
}
|
||||||
|
binmode URANDOM;
|
||||||
|
my $rand;
|
||||||
|
my $rlen = read(URANDOM, $rand, $size);
|
||||||
|
close(FILE);
|
||||||
|
unless(defined($rand) && ($rlen == $size)) {
|
||||||
|
ERROR "Failed to read from /dev/urandom: $!";
|
||||||
|
return undef;
|
||||||
|
}
|
||||||
|
|
||||||
|
if($format eq 'hex') {
|
||||||
|
my $hex = unpack('H*', $rand);
|
||||||
|
die unless(length($hex) == ($size * 2)); # paranoia check
|
||||||
|
return $hex;
|
||||||
|
}
|
||||||
|
elsif($format eq 'bin') {
|
||||||
|
return $rand;
|
||||||
|
}
|
||||||
|
die "unsupported format";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
sub btr_tree($$)
|
sub btr_tree($$)
|
||||||
{
|
{
|
||||||
my $vol = shift;
|
my $vol = shift;
|
||||||
|
@ -2781,6 +2849,9 @@ sub config_encrypt_hash($$)
|
||||||
type => $encrypt_type,
|
type => $encrypt_type,
|
||||||
keyring => config_key($config, "gpg_keyring"),
|
keyring => config_key($config, "gpg_keyring"),
|
||||||
recipient => config_key($config, "gpg_recipient"),
|
recipient => config_key($config, "gpg_recipient"),
|
||||||
|
iv_size => config_key($config, "openssl_iv_size"),
|
||||||
|
ciphername => config_key($config, "openssl_ciphername"),
|
||||||
|
keyfile => config_key($config, "openssl_keyfile"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -428,18 +428,37 @@ raw_target_compress_level default|<number>
|
||||||
.PP
|
.PP
|
||||||
raw_target_compress_threads default|<number>
|
raw_target_compress_threads default|<number>
|
||||||
.PP
|
.PP
|
||||||
raw_target_encrypt gpg|no
|
|
||||||
.PP
|
|
||||||
raw_target_split <size>|no
|
raw_target_split <size>|no
|
||||||
.PP
|
.PP
|
||||||
raw_target_block_size <number> (defaults to 128K)
|
raw_target_block_size <number> (defaults to 128K)
|
||||||
.PP
|
.PP
|
||||||
|
raw_target_encrypt gpg|openssl_enc|no
|
||||||
|
.RE
|
||||||
|
.PD
|
||||||
|
.PP
|
||||||
|
Additional options for "raw_target_encrypt gpg":
|
||||||
|
.PP
|
||||||
|
.RS 4
|
||||||
gpg_keyring <file>
|
gpg_keyring <file>
|
||||||
|
.PD 0
|
||||||
.PP
|
.PP
|
||||||
gpg_recipient <name>
|
gpg_recipient <name>
|
||||||
.RE
|
.RE
|
||||||
.PD
|
.PD
|
||||||
.PP
|
.PP
|
||||||
|
Additional options for "raw_target_encrypt openssl_enc" (\fIvery
|
||||||
|
experimental\fR):
|
||||||
|
.PP
|
||||||
|
.RS 4
|
||||||
|
openssl_ciphername <name> (defaults to "aes-256-cbc")
|
||||||
|
.PD 0
|
||||||
|
.PP
|
||||||
|
openssl_iv_size <size-in-bytes>|no (depends on selected cipher)
|
||||||
|
.PP
|
||||||
|
openssl_keyfile <file>|no
|
||||||
|
.RE
|
||||||
|
.PD
|
||||||
|
.PP
|
||||||
Raw backups consist of two files: the main data file containing the
|
Raw backups consist of two files: the main data file containing the
|
||||||
btrfs send stream, and a sidecar file ".info" containing metadata:
|
btrfs send stream, and a sidecar file ".info" containing metadata:
|
||||||
.RS 4
|
.RS 4
|
||||||
|
|
Loading…
Reference in New Issue