3

I'm trying to implement a MySQL database restore from a GPG-encrypted file.

The following works perfectly well:

my $status = system(
    "gpg --pinentry-mode loopback --passphrase $passphrase --decrypt $my_encrypted_backup_file"
  . " | "
  . "mysql --host=myhost --user=myuser --password=mysecret mydatabase"
);

assuming no error conditions.

However, if an error condition occurs during the first process (such as an incorrect $passphrase), $status == 0 which erroneously indicates success. I understand this is because the status is returned from the second process, the mysql process.

Is there a generalized way, using system(), to either obtain the status from all the piped-together processes, or to somehow detect an error if any one such process fails?

BTW, I have tested gpg by itself (without its output being piped into mysql) and it does return an error code when an incorrect $passphrase is entered.

A workaround might be some option flag in mysql that returns an error when it receives nothing from gpg. Another workaround is to break up the processes and use a tmp file of some sort. However, I'd love a more generalized solution. Thanks!

ikegami
  • 367,544
  • 15
  • 269
  • 518
yahermann
  • 1,539
  • 1
  • 12
  • 33

2 Answers2

3

If you need fine control like that, don't use the shell.

Calls to mysql can be replaced with using the DBI and DBD::mysql libraries. gpg can be replaced with Crypt::GPG.

If this is not possible, do the piping yourself with open and its |- and -| modes.

open(
    my $gpg_out,
    "-|",
    "gpg --pinentry-mode loopback --passphrase $passphrase --decrypt $my_encrypted_backup_file"
) or die "Can't run gpg: $!";

open(
    my $mysql_in,
    "|-",
    "mysql --host=myhost --user=myuser --password=mysecret mydatabase"
) or die "Can't run mysql: $!";

while(my $line = <$gpg_out>) {
    print $mysql_in $line;
}

close $gpg_out;
close $mysql_in;
Schwern
  • 153,029
  • 25
  • 195
  • 336
  • Thanks for the quick and detailed answer! However, `or die ...` is not catching error returned by `gpg`. May have something to do with a child process being created by `-|`, as described in https://perldoc.perl.org/functions/open.html. Just checking `$!` appears to work. Robust enough? – yahermann Jan 07 '19 at 19:11
  • @yahermann `open ... or die` will catch errors when the program is initially executed. Errors while reading or writing have to be caught separately. `print` does return whether it succeeds or not, so you can check that. And you can use a combination of `eof` and [`readline`](https://perldoc.perl.org/functions/readline.html) instead of `<>` to be more robust about reading. As you can see this rapidly gets complicated. I recommend you use the modules or perhaps [IPC::Run](https://metacpan.org/pod/IPC::Run). – Schwern Jan 07 '19 at 19:14
  • Further testing without error conditions returns `gpg: error writing to '-': Broken pipe`. So unless I've goofed it appears your code snippet won't work. Also tried using `eof` and `readline`, and checking return value of `print`, and error condition is still not caught. Sigh........ I will try `IRC::Run`. – yahermann Jan 07 '19 at 20:24
  • @yahermann Something got goofed up. For example, `perl -wle 'open my $fh, "-|", "gpg --version" or die $!; print <$fh>'` works fine. May I again suggest using the modules? – Schwern Jan 07 '19 at 22:40
  • re modules: of course you're absolutely right that's the best way. However I already burned several hours yesterday, getting finicky gpg to work. I've reached the end of the line, and your suggestion to use IPC::Run does work well enough (thank you again!). – yahermann Jan 07 '19 at 22:57
  • @yahermann I'm glad you got it working. Here's some further reading. https://www.wikiwand.com/en/Sunk_cost – Schwern Jan 08 '19 at 01:55
  • 3
    Re "*However, `or die ...` is not catching error returned by `gpg`.*", It does if used on the `close`. Note that you'll avoid the code injection bugs, get better error reporting and avoid creating a shell if you use `open(my $gpg_out, "-|", "gpg", "--pinentry-mode", "loopback", ...)` – ikegami Jan 08 '19 at 04:51
  • @Schwern LOL, here's one for ya https://en.wikipedia.org/wiki/Diminishing_returns. Touche :-). All kidding aside, thank you for helping me out with this! – yahermann Jan 09 '19 at 02:12
2

Thanks to @Schwern for suggesting IPC::Run. Here is a working and tested solution:

use IPC::Run qw( run );

my $gpg = [
  "gpg",
  "--pinentry-mode=loopback",
  "--passphrase=$my_passphrase",
  "--decrypt",
  $my_backupfilepath
];

my $mysql = [
  "mysql",
  "--host=$mysql_host"
  "--user=$mysql_user"
  "--password=$mysql_pass"
  $mysql_dbname
];

run( $gpg, '|', $mysql ) || die "Error";

I still can't manage to capture a detailed error message, and I'm still seeing overly chatty output from gpg & mysql.... but alas I've spent enough time battling Perl and GPG already! Improvements gladly accepted.

Unrelated to the core question, but for anyone using this recipe as is... to get GPG 2.1+ to accept a passphrase via commandline, and to not cache it, you must add the following to gpg-agent.conf:

allow-loopback-pinentry
default-cache-ttl 1
max-cache-ttl 1

Source: https://wiki.archlinux.org/index.php/GnuPG#pinentry

yahermann
  • 1,539
  • 1
  • 12
  • 33