GnuPG-Crypticle

 view release on metacpan or  search on metacpan

lib/GnuPG/Crypticle.pm  view on Meta::CPAN

package GnuPG::Crypticle;
$GnuPG::Crypticle::VERSION = '0.023';
# ABSTRACT: (DEPRECATED) use GnuPG::Interface instead!
# KEYWORDS: deprecated
use namespace::autoclean;
use Moose;
use Fcntl qw//;
use File::Copy qw//;
use File::stat;
use File::Path qw/make_path/;
use File::Spec qw//;
use IO::Handle;
has 'gpg_bin' => (
  is => 'ro',
  isa => 'Str',
  default => '/usr/bin/gpg',
  documentation => 'path to gpg binary',
);

has 'gpg_home' => (
  is => 'ro',
  isa => 'Str',
  required => 1,
  lazy => 1,
  default => sub { return "$ENV{HOME}/.gnupg"; },
  documentation => 'Home directory for GnuPG files (pubring, secring, trustdb)',
);

has 'gpg_pass_file' => (
  is => 'ro',
  isa => 'Str|FileHandle',
  required => 0,
  predicate => 'has_gpg_pass_file',
  documentation => 'passphrase file for decrypting secret keys',
);

has 'gpg_temp_home' => (
  is => 'ro',
  isa => 'Str',
  required => 0,
  predicate => 'has_gpg_temp_home',
  documentation => 'path to temp home',
);

has '_passphrase_fh' => (
  is => 'ro',
  isa => 'FileHandle',
  lazy => 1,
  builder => '_open_passphrase_file',
  documentation => 'filehandle to passphrase file',
);
has '_null_fh' => (
  is => 'ro',
  isa => 'FileHandle',
  lazy => 1,
  builder => '_open_dev_null',
  documentation => 'filehandle to /dev/null',
);

sub BUILD {
  my $self = shift;
  if ($self->has_gpg_temp_home) {
    my $homedir = $self->gpg_home;
    my $gpgdir = $self->gpg_temp_home;
    my $cumask = umask(0077);
    my $mkpatherr;
    unless (
      (-d $gpgdir and -w $gpgdir) or
      File::Path::make_path($gpgdir, {error=>\$mkpatherr}) or
      (-d $gpgdir and -w $gpgdir)
    ) {
      umask($cumask);
      if ($mkpatherr) {
        # ugly but necessary, perldoc File::Path for info
        my $k = (keys %{$mkpatherr->[0]})[0];
        $mkpatherr = $mkpatherr->[0]->{$k};
      }
      else {
        $mkpatherr = "$!";
      }
      umask($cumask);
      die "Unable to create gpg_temp_home '$gpgdir': $mkpatherr";
    }
    for my $f (qw/secring.gpg trustdb.gpg pubring.gpg/) {
      my $file = File::Spec->catfile($homedir, $f);
      unless (File::Copy::cp($file, $gpgdir)) {
        umask($cumask);
        die "Failed to copy '$file' to '$gpgdir': $!";
      }
    }
    File::Copy::cp(File::Spec->catfile($homedir, 'gpg.conf'), $gpgdir);
    umask($cumask);
  }
}

sub decrypt {
  my ($self, %opts) = @_;
  $opts{gpg_args} ||= [];
  push(@{$opts{gpg_args}}, '-d');
  return $self->call_gpg(%opts);
}

sub encrypt {
  my ($self, %opts) = @_;
  $opts{gpg_args} ||= [];
  my $rcpt = delete $opts{rcpt};
  push(@{$opts{gpg_args}}, '-r', $rcpt, '-e');
  return $self->call_gpg(%opts);
}

sub detect_encryption {
  my ($self, %opts) = @_;
  my $fh;
  if (ref($opts{file}) and defined(fileno($opts{file}))) {
    $fh = $opts{file};
  }
  elsif (!open($fh, '<:raw', $opts{file})) {
    die "Failed detecting encryption: $!";
  }
  my $stat = stat($fh);
  # 100 is arbitrary, but if less than 100 bytes could it be an encrypted file?
  # don't go below what is read in for magic (64)
  if ($stat->size > 100) {
    # read in 64 bytes, long enough for the magic test
    my ($magic,$buffer,$bytes) = ('','',0);
    while ($bytes < 64) {
      $bytes += read($fh, $buffer, 64);
      if (!defined($bytes)) {
        die "Read error: $!";
      }
      elsif (!$bytes) {
        # old mcdonald had a farm, e i...
        $! = 5;
        die "Reached EOF on before 64 bytes, though stat said size over 100";
      }
      else {
        $magic .= $buffer;
      }
    }
    if (
      $magic =~ /^\xa6\x00/ or
      $magic =~ /^\x85[\x01\x02\x04]/ or
      $magic =~ /^-----BEGIN\x20PGP\x20(SIGNED\x20)?MESSAGE-/
    ) {
      return 1;
    }
  }
  return 0;
}

sub call_gpg {
  my ($self, %opts) = @_;
  my $dest = delete $opts{dst};
  my $error = delete $opts{err};
  my $source = delete $opts{src};
  my ($close_dest, $close_error, $close_source);
  # using std filehandles for i/o lets us completely ignore dealing with
  # close-on-exec
  my ($stdout,$stderr,$stdin);
  if ($dest) {
    if (defined(fileno($dest))) {
      unless(binmode($dest)) {
        die "Failed to flush dest handle to raw: $!";
      }
    }
    else {
      my $file = $dest;
      $dest = undef;
      unless (open($dest, '>:raw', $file)) {
        die "Failed to open dest file '$file': $!";
      }
      $close_dest = 1;
    }
    if (defined(fileno(STDOUT))) {
      unless (open($stdout, ">&", \*STDOUT)) {
        die "Failed to dup stdout: $!";
      }
    }
    unless (open(STDOUT, ">&", $dest)) {
      die "Failed to dup over STDOUT: $!";
    }
  }
  if ($error) {
    if (defined(fileno($error))) {
      unless (binmode($error)) {
        die "failed to flush error handle to raw: $!";
      }
    }
    else {
      my $file = $error;
      $error = undef;
      unless (open($error, '>>:raw', $file)) {
        die "Failed to open error file '$file': $!";
      }
      $close_error = 1;
    }
    if (defined(fileno(STDERR))) {
      unless (open($stderr, ">&", \*STDERR)) {
        die "Failed to dup stderr: $!";
      }
    }
    unless (open(STDERR, ">&", $error)) {
      die "Failed to dup over STDERR: $!";
    }
  }
  if ($source) {
    if (defined(fileno($source))) {
      unless (binmode($source)) {
        die "Failed to flush source handle to raw: $!";
      }
    }
    else {
      my $file = $source;
      $source = undef;
      unless (open($source, '<:raw', $file)) {
        die "Failed to open source file '$file': $!";
      }
      $close_source = 1;
    }
    if (defined(fileno(STDIN))) {
      unless (open($stdin, "<&", \*STDIN)) {
        die "Failed to dup stdin: $!";
      }
    }
    unless (open(STDIN, "<&", $source)) {
      die "Failed to dup over STDIN: $!";
    }
  }
  if ($self->has_gpg_pass_file) {
    unshift(@{$opts{gpg_args}}, '--passphrase-fd', fileno($self->_passphrase_fh));
  }
  else {
    unshift(@{$opts{gpg_args}}, '--passphrase-fd', fileno($self->_null_fh));
  }
  my $homedir;
  if ($self->has_gpg_temp_home) {
    unshift(@{$opts{gpg_args}}, '--homedir', $self->gpg_temp_home);
  }
  else {
    unshift(@{$opts{gpg_args}}, '--homedir', $self->gpg_home);
  }
  my $gpg_fail;
  unless (system($self->gpg_bin, '--batch', '--no-tty', @{$opts{gpg_args}}) == 0) {
    if ($! == 0) {
      $gpg_fail = "Failed to execute gpg: $?";
    }
    else {
      $gpg_fail = "gpg call failed: $?";
    }
  }
  if ($self->has_gpg_pass_file) {
    seek($self->_passphrase_fh, 0, 0);
  }
  if ($stdin) {
    unless (open(STDIN, "<&", $stdin)) {
      die "Failed to restore STDIN";
    }
  }
  close($source) if $close_source;
  if ($stderr) {
    unless (open(STDERR, ">&", $stderr)) {
      die "Failed to restore STDERR";
    }
  }
  close($error) if $close_error;
  if ($stdout) {
    unless (open(STDOUT, ">&", $stdout)) {
      die "Failed to restore STDOUT";
    }
  }
  close($dest) if $close_dest;
  die $gpg_fail if $gpg_fail;
  return 1;
}

sub _open_passphrase_file {
  my $self = shift;
  if (my $file = $self->gpg_pass_file) {
    if (ref($file)) {
      my $flags;
      unless ($flags = fcntl($file, Fcntl::F_GETFD, 0)) {
        die "fcntl F_GETFD failed: $!";
      }
      unless (fcntl($file, Fcntl::F_SETFD, $flags & ~Fcntl::FD_CLOEXEC)) {
        die "fcntl F_SETFD failed: $!";
      }
      return $file;
    }
    else {
      my $fh;
      unless (open($fh, '<', $file)) {
        die "Failed to open passphrase file: $!";
      }
      my $flags;
      unless ($flags = fcntl($fh, Fcntl::F_GETFD, 0)) {
        die "fcntl F_GETFD failed: $!";
      }
      unless (fcntl($fh, Fcntl::F_SETFD, $flags & ~Fcntl::FD_CLOEXEC)) {
        die "fcntl F_SETFD failed: $!";
      }
      return $fh;
    }
  }
}

sub _open_dev_null {
  my $fh;
  unless (open($fh, '<', File::Spec->devnull)) {
    die "Failed to open /dev/null: $!";
  }
  return $fh;
}

__PACKAGE__->meta->make_immutable();

1;

__END__

=pod

=encoding UTF-8

=head1 NAME

GnuPG::Crypticle - (DEPRECATED) use GnuPG::Interface instead!

=head1 VERSION

version 0.023

=head1 SYNOPSIS

Stop reading here, and go use L<GnuPG::Interface> instead.

    use GnuPG::Crypticle;

    my $crypticle = GnuPG::Crypticle->new(gpg_home => /home/me/.gnupg);
    $crypticle->encrypt(src => '/tmp/sourcefile.txt', dst => '/tmp/destfile.gpg', rcpt => 'ABCD0123');
    ...

=head1 DEPRECATION

This module should be considered deprecated and unmaintained. It was a stop-gap
-- albeit not a very good one -- when the author had no better option to use
gpg2 (L<GnuPG> only works with gpg1). L<GnuPG::Interface> is a much better
option. Please use that module instead!

=head1 ATTRIBUTES

=head2 gpg_bin

full path to gpg binary

=head2 gpg_home

location of the .gnupg directory gpg should use

=head2 gpg_pass_file

plaintext file containing the passphrase used with any secret keys

=head2 gpg_temp_home

path to use as temporary home

=head1 METHODS

Parameters are passed to all methods as a key/value list (hash) e.g.,

subroutine(key1 => val1, key2 => val2);

=head2 BUILD

During object initialization, copies of master gpg keyrings are made in a
temporary directory to prevent locking and corruption problems. A restart of
the application is necessary if there are key ring changes. Dies on failure.

=head2 decrypt

Encrypts from a source to destination file. Croaks on decryption failure,
including signature failure.

parameters:

=over 2

=item src

file name or handle to be decrypted

=item dst

file name or handle to which decrypted output is sent

=back

returns:

=over 2

valid signature if present, or true

=back

=head2 encrypt

Dies on failure

parameters:

=over 2

=item src

file name or handle to be encrypted

=item dst

file name or handle to which encrypted output is sent

=item gpg_args

arguments passed directly to gpg execution

=back

returns:

=over 2

valid signature if present, or true

=back

=head2 detect_encryption

Dies on failure. Detects pgp or gpg decryption the same as mime magic does.

This is nowhere near complete or reliable. For best results, just try to
decrypt.

parameters:

=over 2

=item file

file name or handle from which to detect encryption

=back

=head2 call_gpg

(private) calls gpg command with necessary options

=head2 _open_passphrase_file

(private) Opens the passphrase file.

=head2 _open_dev_null

(private) returns a filehandle to /dev/null

=head1 SEE ALSO

This should be read "see instead." L<GnuPG::Interface>

=head1 AUTHOR

Brad Barden <b at 13os.net>

=head1 COPYRIGHT AND LICENSE

This software is Copyright (c) 2016 by Brad Barden.

This is free software, licensed under:

  The ISC License

=cut



( run in 0.817 second using v1.01-cache-2.11-cpan-e1769b4cff6 )