]> Dogcows Code - chaz/groupsecret/blob - lib/App/GroupSecret.pm
ce85e9db419432d9576e6be1d48b042c31deebda
[chaz/groupsecret] / lib / App / GroupSecret.pm
1 package App::GroupSecret;
2 # ABSTRACT: A simple tool for maintaining a shared group secret
3
4 =head1 DESCRIPTION
5
6 This module is part of the command-line interface for managing keyfiles.
7
8 See L<groupsecret> for documentation.
9
10 =cut
11
12 use warnings;
13 use strict;
14
15 our $VERSION = '9999.999'; # VERSION
16
17 use App::GroupSecret::Crypt qw(generate_secure_random_bytes read_openssh_key_fingerprint);
18 use App::GroupSecret::File;
19 use Getopt::Long qw(GetOptionsFromArray);
20 use MIME::Base64;
21 use Pod::Usage;
22 use namespace::clean;
23
24 =method new
25
26 $script = App::GroupSecret->new;
27
28 Construct a new script object.
29
30 =cut
31
32 sub new {
33 my $class = shift;
34 return bless {}, $class;
35 }
36
37 =method main
38
39 $script->main(@ARGV);
40
41 Run a command with the given command-line arguments.
42
43 =cut
44
45 sub main {
46 my $self = shift;
47 my @args = @_;
48
49 my $filepath = '';
50 my $help = 0;
51 my $man = 0;
52 my $version = 0;
53 my $private_key = '';
54
55 # Parse options using pass_through so that we can pick out the global
56 # options, wherever they are in the arg list, and leave the rest to be
57 # parsed by each individual command.
58 Getopt::Long::Configure('pass_through');
59 GetOptionsFromArray(
60 \@args,
61 'file|f=s' => \$filepath,
62 'help|h|?' => \$help,
63 'manual|man' => \$man,
64 'private-key|k=s' => \$private_key,
65 'version|v' => \$version,
66 ) or pod2usage(2);
67 Getopt::Long::Configure('default');
68
69 pod2usage(-exitval => 1, -verbose => 99, -sections => [qw(SYNOPSIS OPTIONS COMMANDS)]) if $help;
70 pod2usage(-verbose => 2) if $man;
71 return print "groupsecret ${VERSION}\n" if $version;
72
73 $self->{private_key} = $private_key if $private_key;
74 $self->{filepath} = $filepath if $filepath;
75
76 my %commands = (
77 add_key => 'add_key',
78 add_keys => 'add_key',
79 change_secret => 'set_secret',
80 delete_key => 'delete_key',
81 delete_keys => 'delete_key',
82 list_keys => 'list_keys',
83 print => 'print_secret',
84 print_secret => 'print_secret',
85 remove_key => 'delete_key',
86 remove_keys => 'delete_key',
87 set_secret => 'set_secret',
88 show_secret => 'print_secret',
89 update_secret => 'set_secret',
90 );
91
92 unshift @args, 'print' if !@args || $args[0] =~ /^-/;
93
94 my $command = shift @args;
95 my $lookup = $command;
96 $lookup =~ s/-/_/g;
97 my $method = '_action_' . ($commands{$lookup} || '');
98
99 if (!$self->can($method)) {
100 warn "Unknown command: $command\n";
101 pod2usage(2);
102 }
103
104 $self->$method(@args);
105 }
106
107 =method filepath
108
109 $filepath = $script->filepath;
110
111 Get the path to the keyfile.
112
113 =cut
114
115 sub filepath {
116 shift->{filepath} ||= $ENV{GROUPSECRET_KEYFILE} || 'groupsecret.yml';
117 }
118
119 =method file
120
121 $file = $script->file;
122
123 Get the L<App::GroupSecret::File> instance for the keyfile.
124
125 =cut
126
127 sub file {
128 my $self = shift;
129 return $self->{file} ||= App::GroupSecret::File->new($self->filepath);
130 }
131
132 =method private_key
133
134 $filepath = $script->private_key;
135
136 Get the path to a private key used to decrypt the keyfile.
137
138 =cut
139
140 sub private_key {
141 shift->{private_key} ||= $ENV{GROUPSECRET_PRIVATE_KEY} || "$ENV{HOME}/.ssh/id_rsa";
142 }
143
144 sub _action_print_secret {
145 my $self = shift;
146
147 my $decrypt = 1;
148 GetOptionsFromArray(
149 \@_,
150 'decrypt!' => \$decrypt,
151 ) or pod2usage(2);
152
153 my $file = $self->file;
154 my $filepath = $file->filepath;
155 die "No keyfile '$filepath' exists -- use the \`add-key' command to create one.\n"
156 unless -e $filepath && !-d $filepath;
157 die "No secret in keyfile '$filepath' exists -- use the \`set-secret' command to set one.\n"
158 if !$file->secret;
159
160 if ($decrypt) {
161 my $private_key = $self->private_key;
162 my $secret = $file->decrypt_secret(private_key => $private_key) or die "No secret.\n";
163 print $secret;
164 }
165 else {
166 print $file->secret;
167 }
168 }
169
170 sub _action_set_secret {
171 my $self = shift;
172
173 my $keep_passphrase = 0;
174 GetOptionsFromArray(
175 \@_,
176 'keep-passphrase!' => \$keep_passphrase,
177 ) or pod2usage(2);
178
179 my $secret_spec = shift;
180 if (!$secret_spec) {
181 warn "You must specify a secret to set.\n";
182 pod2usage(2);
183 }
184
185 my $passphrase;
186 my $secret;
187
188 if ($secret_spec =~ /^rand:(\d+)$/i) {
189 my $rand = encode_base64(generate_secure_random_bytes($1), '');
190 $secret = \$rand;
191 }
192 elsif ($secret_spec eq '-') {
193 my $in = do { local $/; <STDIN> };
194 $secret = \$in;
195 }
196 elsif ($secret_spec =~ /^file:(.*)$/i) {
197 $secret = $1;
198 }
199 else {
200 $secret = $secret_spec;
201 }
202
203 my $file = $self->file;
204
205 if ($keep_passphrase) {
206 my $private_key = $self->private_key;
207 $passphrase = $file->decrypt_secret_passphrase($private_key);
208 $file->encrypt_secret($secret, $passphrase);
209 }
210 else {
211 $passphrase = generate_secure_random_bytes(32);
212 $file->encrypt_secret($secret, $passphrase);
213 $file->encrypt_secret_passphrase($passphrase);
214 }
215
216 $file->save;
217 }
218
219 sub _action_add_key {
220 my $self = shift;
221
222 my $embed = 0;
223 my $update = 0;
224 GetOptionsFromArray(
225 \@_,
226 'embed' => \$embed,
227 'update|u' => \$update,
228 ) or pod2usage(2);
229
230 my $file = $self->file;
231 my $keys = $file->keys;
232
233 my $opts = {embed => $embed};
234
235 for my $public_key (@_) {
236 my $info = read_openssh_key_fingerprint($public_key);
237
238 if ($keys->{$info->{fingerprint}} && !$update) {
239 my $formatted_key = $file->format_key($info);
240 print "SKIP\t$formatted_key\n";
241 next;
242 }
243
244 if ($file->secret && !$opts->{passphrase}) {
245 my $private_key = $self->private_key;
246 my $passphrase = $file->decrypt_secret_passphrase($private_key);
247 $opts->{passphrase} = $passphrase;
248 }
249
250 local $opts->{fingerprint_info} = $info;
251 my ($fingerprint, $key) = $file->add_key($public_key, $opts);
252
253 local $key->{fingerprint} = $fingerprint;
254 my $formatted_key = $file->format_key($key);
255 print "ADD\t$formatted_key\n";
256 }
257
258 $file->save;
259 }
260
261 sub _action_delete_key {
262 my $self = shift;
263
264 my $file = $self->file;
265
266 for my $fingerprint (@_) {
267 if ($fingerprint =~ s/^(?:MD5|SHA1|SHA256)://) {
268 $fingerprint =~ s/://g;
269 }
270 else {
271 my $info = read_openssh_key_fingerprint($fingerprint);
272 $fingerprint = $info->{fingerprint};
273 }
274
275 my $key = $file->keys->{$fingerprint};
276 $file->delete_key($fingerprint) if $key;
277
278 local $key->{fingerprint} = $fingerprint;
279 my $formatted_key = $file->format_key($key);
280 print "DELETE\t$formatted_key\n";
281 }
282
283 $file->save;
284 }
285
286 sub _action_list_keys {
287 my $self = shift;
288
289 my $file = $self->file;
290 my $keys = $file->keys;
291
292 while (my ($fingerprint, $key) = each %$keys) {
293 local $key->{fingerprint} = $fingerprint;
294 my $formatted_key = $file->format_key($key);
295 print "$formatted_key\n";
296 }
297 }
298
299 1;
This page took 0.048651 seconds and 3 git commands to generate.