package App::GroupSecret; # ABSTRACT: A simple tool for maintaining a shared group secret =head1 DESCRIPTION This module is part of the command-line interface for managing keyfiles. See L for documentation. =cut use warnings; use strict; our $VERSION = '9999.999'; # VERSION use App::GroupSecret::Crypt qw(generate_secure_random_bytes read_openssh_key_fingerprint); use App::GroupSecret::File; use Getopt::Long qw(GetOptionsFromArray); use MIME::Base64; use Pod::Usage; use namespace::clean; =method new $script = App::GroupSecret->new; Construct a new script object. =cut sub new { my $class = shift; return bless {}, $class; } =method main $script->main(@ARGV); Run a command with the given command-line arguments. =cut sub main { my $self = shift; my @args = @_; my $filepath = ''; my $help = 0; my $man = 0; my $version = 0; my $private_key = ''; # Parse options using pass_through so that we can pick out the global # options, wherever they are in the arg list, and leave the rest to be # parsed by each individual command. Getopt::Long::Configure('pass_through'); GetOptionsFromArray( \@args, 'file|f=s' => \$filepath, 'help|h|?' => \$help, 'manual|man' => \$man, 'private-key|k=s' => \$private_key, 'version|v' => \$version, ) or pod2usage(2); Getopt::Long::Configure('default'); pod2usage(-exitval => 1, -verbose => 99, -sections => [qw(SYNOPSIS OPTIONS COMMANDS)]) if $help; pod2usage(-verbose => 2) if $man; return print "groupsecret ${VERSION}\n" if $version; $self->{private_key} = $private_key if $private_key; $self->{filepath} = $filepath if $filepath; my %commands = ( add_key => 'add_key', add_keys => 'add_key', change_secret => 'set_secret', delete_key => 'delete_key', delete_keys => 'delete_key', list_keys => 'list_keys', print => 'print_secret', print_secret => 'print_secret', remove_key => 'delete_key', remove_keys => 'delete_key', set_secret => 'set_secret', show_secret => 'print_secret', update_secret => 'set_secret', ); unshift @args, 'print' if !@args || $args[0] =~ /^-/; my $command = shift @args; my $lookup = $command; $lookup =~ s/-/_/g; my $method = '_action_' . ($commands{$lookup} || ''); if (!$self->can($method)) { warn "Unknown command: $command\n"; pod2usage(2); } $self->$method(@args); } =method filepath $filepath = $script->filepath; Get the path to the keyfile. =cut sub filepath { shift->{filepath} ||= $ENV{GROUPSECRET_KEYFILE} || 'groupsecret.yml'; } =method file $file = $script->file; Get the L instance for the keyfile. =cut sub file { my $self = shift; return $self->{file} ||= App::GroupSecret::File->new($self->filepath); } =method private_key $filepath = $script->private_key; Get the path to a private key used to decrypt the keyfile. =cut sub private_key { shift->{private_key} ||= $ENV{GROUPSECRET_PRIVATE_KEY} || "$ENV{HOME}/.ssh/id_rsa"; } sub _action_print_secret { my $self = shift; my $decrypt = 1; GetOptionsFromArray( \@_, 'decrypt!' => \$decrypt, ) or pod2usage(2); my $file = $self->file; die "No secret in file -- use the \`set-secret' command to set one.\n" if !$file->secret; if ($decrypt) { my $private_key = $self->private_key; my $secret = $file->decrypt_secret(private_key => $private_key) or die "No secret.\n"; print $secret; } else { print $file->secret; } } sub _action_set_secret { my $self = shift; my $keep_passphrase = 0; GetOptionsFromArray( \@_, 'keep-passphrase!' => \$keep_passphrase, ) or pod2usage(2); my $secret_spec = shift; if (!$secret_spec) { warn "You must specify a secret to set.\n"; pod2usage(2); } my $passphrase; my $secret; if ($secret_spec =~ /^rand:(\d+)$/i) { my $rand = encode_base64(generate_secure_random_bytes($1), ''); $secret = \$rand; } elsif ($secret_spec eq '-') { my $in = do { local $/; }; $secret = \$in; } elsif ($secret_spec =~ /^file:(.*)$/i) { $secret = $1; } else { $secret = $secret_spec; } my $file = $self->file; if ($keep_passphrase) { my $private_key = $self->private_key; $passphrase = $file->decrypt_secret_passphrase($private_key); $file->encrypt_secret($secret, $passphrase); } else { $passphrase = generate_secure_random_bytes(32); $file->encrypt_secret($secret, $passphrase); $file->encrypt_secret_passphrase($passphrase); } $file->save; } sub _action_add_key { my $self = shift; my $embed = 0; my $update = 0; GetOptionsFromArray( \@_, 'embed' => \$embed, 'update|u' => \$update, ) or pod2usage(2); my $file = $self->file; my $keys = $file->keys; my $opts = {embed => $embed}; for my $public_key (@_) { my $info = read_openssh_key_fingerprint($public_key); if ($keys->{$info->{fingerprint}} && !$update) { my $formatted_key = $file->format_key($info); print "SKIP\t$formatted_key\n"; next; } if ($file->secret && !$opts->{passphrase}) { my $private_key = $self->private_key; my $passphrase = $file->decrypt_secret_passphrase($private_key); $opts->{passphrase} = $passphrase; } local $opts->{fingerprint_info} = $info; my ($fingerprint, $key) = $file->add_key($public_key, $opts); local $key->{fingerprint} = $fingerprint; my $formatted_key = $file->format_key($key); print "ADD\t$formatted_key\n"; } $file->save; } sub _action_delete_key { my $self = shift; my $file = $self->file; for my $fingerprint (@_) { if ($fingerprint =~ s/^(?:MD5|SHA1|SHA256)://) { $fingerprint =~ s/://g; } else { my $info = read_openssh_key_fingerprint($fingerprint); $fingerprint = $info->{fingerprint}; } my $key = $file->keys->{$fingerprint}; $file->delete_key($fingerprint) if $key; local $key->{fingerprint} = $fingerprint; my $formatted_key = $file->format_key($key); print "DELETE\t$formatted_key\n"; } $file->save; } sub _action_list_keys { my $self = shift; my $file = $self->file; my $keys = $file->keys; while (my ($fingerprint, $key) = each %$keys) { local $key->{fingerprint} = $fingerprint; my $formatted_key = $file->format_key($key); print "$formatted_key\n"; } } 1;