aboutsummaryrefslogtreecommitdiff
path: root/bin/croncrypt
blob: 68854e76ae992331413e42d79c22720a20125ea8 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
#!/usr/bin/env perl

#
# croncrypt: Wrapper to sign and encrypt cron output and errors with PGP/MIME
# before sending them to the default MAILTO destination.
#
#     CRONCRYPT_KEYID=0x0A1B2C3D4E5F6G7H
#     CRONCRYPT_PASSPHRASE=hibbityboo
#     MAILTO=tom@sanctum.geek.nz
#     0 1 * * *  tom  croncrypt rsync /home/tom/important-file /home/backups
#
# The main design goal is simplicity; just whack a «croncrypt» in front of all
# your cron tasks, provided they don't use pipes or stderr/stdout redirects,
# in which case you should consider putting it all into a script file anyway.
#
# Don't use your own GPG key for signing! Create a dedicated key just for
# croncrypt, and sign it locally with «gpg --lsign» maybe.
#
# Author: Tom Ryder <tom@sanctum.geek.nz>
# Copyright: 2014 Sanctum
# License: Artistic <http://dev.perl.org/licenses/artistic.html>
#
# $Id$
#
package Sanctum::Croncrypt;

# Force me to write this properly
use strict;
use warnings;
use utf8;
use autodie;

# Decree minimum Perl version required (v5.8).
use 5.008_001;

# Decree package version to pacify Perl::Critic
our $VERSION = 0.1;

# Pull in some required modules
use Carp;
use IPC::Open3;
use Mail::GnuPG;
use MIME::Entity;
use Symbol 'gensym';

# Bail if run without arguments
if ( !@ARGV ) {
    printf "%s\n", 'USAGE: croncrypt <command>';
    exit 1;
}

# Bail if we don't have the environment variables we need
my @fails = ();
if ( !exists $ENV{'CRONCRYPT_KEYID'} ) {
    push @fails, 'CRONCRYPT_KEYID is not set; set it to your key ID.';
}
if ( !exists $ENV{'CRONCRYPT_PASSPHRASE'} ) {
    push @fails,
      'CRONCRYPT_PASSPHRASE is not set; set it to your key\'s passphrase.';
}
if ( !exists $ENV{'MAILTO'} ) {
    push @fails, 'MAILTO is not set; set it to the message\'s destination.';
}
if (@fails) {
    foreach my $fail (@fails) {
        printf {*STDERR} "croncrypt: FAIL: %s\n", $fail;
    }
    exit 1;
}

# Read details from environment
my $recipient  = $ENV{MAILTO};
my $key        = $ENV{CRONCRYPT_KEYID};
my $passphrase = $ENV{CRONCRYPT_PASSPHRASE};

# Establish filehandles; need to specifically create symbol for stderr
my ( $stdin, $stdout, $stderr );
$stderr = gensym;

# Run the command in the arguments and wait for it to finish
my $pid = open3( $stdin, $stdout, $stderr, @ARGV );
waitpid $pid, 0;

# Read any and all output and errors
my @output = <$stdout>;
my @errors = <$stderr>;

# Close the filehandles, placating Perl::Critic with return checks
close $stdin
  or croak('Could not close stdin');
close $stdout
  or croak('Could not close stdout');
close $stderr
  or croak('Could not close stderr');

# If there was output, mail it
if (@output) {
    my $subject = sprintf 'croncrypt output: %s', join q{ }, @ARGV;
    mail( $subject, \@output );
}

# If there were errors, mail them
if (@errors) {
    my $subject = sprintf 'croncrypt errors: %s', join q{ }, @ARGV;
    mail( $subject, \@errors );
}

# Send the message to the address in $ENV{MAILTO}
sub mail {
    my ( $subject, $content ) = @_;

    # Build MIME object with plaintext message
    my $mime = MIME::Entity->build(
        To      => $recipient,
        Subject => $subject,
        Data    => $content,
    );

    # Encrypt the MIME object
    my $mgpg = Mail::GnuPG->new(
        key        => $key,
        passphrase => $passphrase,
    );
    $mgpg->mime_signencrypt( $mime, $recipient );

    # Send it
    return $mime->send();
}