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