From de7628ac7caec037d5c726d5050777347db7d5b7 Mon Sep 17 00:00:00 2001 From: Axel Burri Date: Fri, 16 Jun 2017 17:04:18 +0200 Subject: [PATCH] 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/backup --- ChangeLog | 1 + btrbk | 83 ++++++++++++++++++++++++++++++++++++++++++++---- doc/btrbk.conf.5 | 23 ++++++++++++-- 3 files changed, 99 insertions(+), 8 deletions(-) diff --git a/ChangeLog b/ChangeLog index f969f74..db16c8c 100644 --- a/ChangeLog +++ b/ChangeLog @@ -9,6 +9,7 @@ btrbk-current * Add "--preserve-snapshots" and "--preserve-backups" options. * Change raw backup format (sidecar file instead of uuid in file). * 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 hashbang; hardcode $PATH only if taint mode is enabled. * Remove "duration" column from transaction_log/transaction_syslog. diff --git a/btrbk b/btrbk index e1cc9d3..ba91849 100755 --- a/btrbk +++ b/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 $timestamp_postfix_match = qr/\.(?[0-9]{4})(?[0-9]{2})(?
[0-9]{2})(T(?[0-9]{2})(?[0-9]{2})((?[0-9]{2})(?(Z|[+-][0-9]{4})))?)?(_(?[0-9]+))?/; # matches "YYYYMMDD[Thhmm[ss+0000]][_NN]" my $raw_postfix_match_DEPRECATED = qr/--(?$uuid_match)(\@(?$uuid_match))?\.btrfs?(\.(?($compress_format_alt)))?(\.(?gpg))?(\.(?split))?(\.(?part))?/; # matches ".btrfs_[@][.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 $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_level => { 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_split => { default => undef, accept => [ "no" ], accept_regexp => qr/^[0-9]+([kmgtpezyKMGTPEZY][bB]?)?$/ }, gpg_keyring => { default => undef, accept_file => { absolute => 1 } }, 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*/ }, @@ -231,6 +234,8 @@ my %raw_info_sort = ( compress => 9, split => 10, encrypt => 11, + cipher => 12, + iv => 13, ); my %url_cache; # map URL to btr_tree node @@ -1379,6 +1384,14 @@ sub btrfs_send_to_file($$$;$$) $target_filename .= '.' . $compression{$compress->{key}}->{format}; 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) { $raw_info{encrypt} = $encrypt->{type}; @@ -1393,7 +1406,6 @@ sub btrfs_send_to_file($$$;$$) # generation faster; however sometimes write operations are not # desired. This option can be used to achieve that with the cost # of slower random generation. - $target_filename .= '.gpg'; 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, ( '--no-default-keyring', '--keyring', $encrypt->{keyring} )) if($encrypt->{keyring}); @@ -1404,6 +1416,35 @@ sub btrfs_send_to_file($$$;$$) 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 { 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{RECEIVED_PARENT_UUID} = $parent_uuid if($parent_uuid); # 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($$) { my $vol = shift; @@ -2781,6 +2849,9 @@ sub config_encrypt_hash($$) type => $encrypt_type, keyring => config_key($config, "gpg_keyring"), 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"), }; } diff --git a/doc/btrbk.conf.5 b/doc/btrbk.conf.5 index f8179a1..10414b4 100644 --- a/doc/btrbk.conf.5 +++ b/doc/btrbk.conf.5 @@ -428,18 +428,37 @@ raw_target_compress_level default| .PP raw_target_compress_threads default| .PP -raw_target_encrypt gpg|no -.PP raw_target_split |no .PP raw_target_block_size (defaults to 128K) .PP +raw_target_encrypt gpg|openssl_enc|no +.RE +.PD +.PP +Additional options for "raw_target_encrypt gpg": +.PP +.RS 4 gpg_keyring +.PD 0 .PP gpg_recipient .RE .PD .PP +Additional options for "raw_target_encrypt openssl_enc" (\fIvery +experimental\fR): +.PP +.RS 4 +openssl_ciphername (defaults to "aes-256-cbc") +.PD 0 +.PP +openssl_iv_size |no (depends on selected cipher) +.PP +openssl_keyfile |no +.RE +.PD +.PP Raw backups consist of two files: the main data file containing the btrfs send stream, and a sidecar file ".info" containing metadata: .RS 4