1 package App
::GroupSecret
::File
;
2 # ABSTRACT: Reading and writing groupsecret keyfiles
8 our $VERSION = '0.304'; # VERSION
10 use App
::GroupSecret
::Crypt
qw(
11 generate_secure_random_bytes
12 read_openssh_public_key
13 read_openssh_key_fingerprint
21 use YAML
::Tiny
qw(LoadFile DumpFile);
24 our $FILE_VERSION = 1;
26 sub _croak
{ require Carp
; Carp
::croak
(@_) }
27 sub _usage
{ _croak
("Usage: @_\n") }
32 my $filepath = shift or _croak
(q{App::GroupSecret::File->new($filepath)});
33 return bless {filepath
=> $filepath}, $class;
37 sub filepath
{ shift-
>{filepath
} }
42 return $self->{info
} ||= do {
43 if (-e
$self->filepath) {
57 version
=> $FILE_VERSION,
64 my $filepath = shift || $self->filepath;
65 my $info = LoadFile
($filepath) || {};
67 $self->{info
} = $info if !$filepath;
74 my $filepath = shift || $self->filepath;
75 DumpFile
($filepath, $self->info);
82 my $info = shift || $self->info;
84 _croak
'Corrupt file: Bad type for root' if !$info || ref $info ne 'HASH';
86 my $version = $info->{version
};
87 _croak
'Unknown file version' if !$version || $version !~ /^\d+$/;
88 _croak
'Unsupported file version' if $FILE_VERSION < $version;
90 _croak
'Corrupt file: Bad type for keys' if ref $info->{keys} ne 'HASH';
92 warn "The file has a secret but no keys to access it!\n" if $info->{secret
} && !%{$info->{keys}};
98 sub keys { shift-
>info->{keys} }
99 sub secret
{ shift-
>info->{secret
} }
100 sub version
{ shift-
>info->{version
} }
105 my $public_key = shift or _usage
(q{$file->add_key($public_key)});
106 my $args = @_ == 1 ? shift : {@_};
108 my $keys = $self->keys;
110 my $info = $args->{fingerprint_info
} || read_openssh_key_fingerprint
($public_key);
111 my $fingerprint = $info->{fingerprint
};
114 comment
=> $info->{comment
},
115 filename
=> basename
($public_key),
116 secret_passphrase
=> undef,
117 type
=> $info->{type
},
120 if ($args->{embed
}) {
121 open(my $fh, '<', $public_key) or die "open failed: $!";
122 $key->{content
} = do { local $/; <$fh> };
123 chomp $key->{content
};
126 $keys->{$fingerprint} = $key;
129 my $passphrase = $args->{passphrase
} || $self->decrypt_secret_passphrase($args->{private_key
});
130 my $ciphertext = encrypt_rsa
(\
$passphrase, $public_key);
131 $key->{secret_passphrase
} = $ciphertext;
134 return wantarray ? ($fingerprint => $key) : $key;
140 my $fingerprint = shift;
141 delete $self->keys->{$fingerprint};
147 my $args = @_ == 1 ? shift : {@_};
149 $args->{passphrase
} || $args->{private_key
} or _usage
(q{$file->decrypt_secret($private_key)});
151 my $passphrase = $args->{passphrase
};
152 $passphrase = $self->decrypt_secret_passphrase($args->{private_key
}) if !$passphrase;
154 my $ciphertext = $self->secret;
155 return decrypt_aes_256_cbc
(\
$ciphertext, $passphrase);
159 sub decrypt_secret_passphrase
{
161 my $private_key = shift or _usage
(q{$file->decrypt_secret_passphrase($private_key)});
163 die "Private key '$private_key' not found.\n" unless -e
$private_key && !-d
$private_key;
165 my $info = read_openssh_key_fingerprint
($private_key);
166 my $fingerprint = $info->{fingerprint
};
168 my $keys = $self->keys;
169 if (my $key = $keys->{$fingerprint}) {
170 return decrypt_rsa
(\
$key->{secret_passphrase
}, $private_key);
173 die "Private key '$private_key' not able to decrypt the keyfile.\n";
179 my $secret = shift or _usage
(q{$file->encrypt_secret($secret)});
180 my $passphrase = shift or _usage
(q{$file->encrypt_secret($secret)});
182 my $ciphertext = encrypt_aes_256_cbc
($secret, $passphrase);
183 $self->info->{secret
} = $ciphertext;
187 sub encrypt_secret_passphrase
{
189 my $passphrase = shift or _usage
(q{$file->encrypt_secret_passphrase($passphrase)});
191 while (my ($fingerprint, $key) = each %{$self->keys}) {
192 local $key->{fingerprint
} = $fingerprint;
193 my $pubkey = $self->find_public_key($key) or die 'Cannot find public key: ' . $self->format_key($key) . "\n";
194 my $ciphertext = encrypt_rsa
(\
$passphrase, $pubkey);
195 $key->{secret_passphrase
} = $ciphertext;
200 sub find_public_key
{
202 my $key = shift or _usage
(q{$file->find_public_key($key)});
204 if ($key->{content
}) {
205 my $temp = File
::Temp-
>new(UNLINK
=> 1);
206 print $temp $key->{content
};
208 $self->{"temp:$key->{fingerprint}"} = $temp;
209 return $temp->filename;
212 my @dirs = split(/:/, $ENV{GROUPSECRET_PATH
} || ".:keys:$ENV{HOME}/.ssh");
213 for my $dir (@dirs) {
214 my $filepath = File
::Spec-
>catfile($dir, $key->{filename
});
215 return $filepath if -e
$filepath && !-d
$filepath;
223 my $key = shift or _usage
(q{$file->format_key($key)});
225 my $fingerprint = $key->{fingerprint
} or _croak
(q{Missing required field in key: fingerprint});
226 my $comment = $key->{comment
} || 'uncommented';
228 if ($fingerprint =~ /^[A-Fa-f0-9]{32}$/) {
229 $fingerprint = 'MD5:' . join(':', ($fingerprint =~ /../g ));
231 elsif ($fingerprint =~ /^[A-Za-z0-9\/\
+]{27}$/) {
232 $fingerprint = "SHA1:$fingerprint";
234 elsif ($fingerprint =~ /^[A-Za-z0-9\/\
+]{43}$/) {
235 $fingerprint = "SHA256:$fingerprint";
238 return "$fingerprint $comment";
251 App::GroupSecret::File - Reading and writing groupsecret keyfiles
259 use App::GroupSecret::File;
261 my $file = App::GroupSecret::File->new('path/to/keyfile.yml');
262 print "File version: " . $file->version, "\n";
264 $file->add_key('path/to/key_rsa.pub');
269 This module provides a programmatic way to manage keyfiles.
271 See L<groupsecret> for the command-line interface.
277 Get the filepath of the keyfile.
283 $file = App::GroupSecret::File->new($filepath);
285 Construct a new keyfile object.
291 Get a raw hashref with the contents of the keyfile.
297 Get a hashref representing an empty keyfile, used for initializing a new keyfile.
302 $info = $file->load($filepath);
304 Load (or reload) the contents of a keyfile.
309 $file->save($filepath);
311 Save the keyfile to disk.
318 Check the file format of a keyfile to make sure this module can understand it.
324 Get a hashref of the keys from a keyfile.
328 $secret = $file->secret;
330 Get the secret from a keyfile as an encrypted string.
334 $version = $file->version
336 Get the file format version.
340 $file->add_key($filepath);
342 Add a key to the keyfile.
346 $file->delete_key($fingerprint);
348 Delete a key from the keyfile.
350 =head2 decrypt_secret
352 $secret = $file->decrypt_secret(passphrase => $passphrase);
353 $secret = $file->decrypt_secret(private_key => $private_key);
355 Get the decrypted secret.
357 =head2 decrypt_secret_passphrase
359 $passphrase = $file->decrypt_secret_passphrase($private_key);
361 Get the decrypted secret passphrase.
363 =head2 encrypt_secret
365 $file->encrypt_secret($secret, $passphrase);
367 Set the secret by encrypting it with a 256-bit passphrase.
369 Passphrase must be 32 bytes.
371 =head2 encrypt_secret_passphrase
373 $file->encrypt_secret_passphrase($passphrase);
375 Set the passphrase by encrypting it with each key in the keyfile.
377 =head2 find_public_key
379 $filepath = $file->find_public_key($key);
381 Get a path to the public key file for a key.
385 $str = $file->format_key($key);
387 Get a one-line summary of a key. Format is "<fingerprint> <comment>".
391 Keyfiles are YAML documents that contains this structure:
399 secret_passphrase: PASSPHRASE...
406 Please report any bugs or feature requests on the bugtracker website
407 L<https://github.com/chazmcgarvey/groupsecret/issues>
409 When submitting a bug or request, please include a test-file or a
410 patch to an existing test-file that illustrates the bug or desired
415 Charles McGarvey <chazmcgarvey@brokenzipper.com>
417 =head1 COPYRIGHT AND LICENSE
419 This software is Copyright (c) 2017 by Charles McGarvey.
421 This is free software, licensed under:
423 The MIT (X11) License