1 package App
::GroupSecret
::File
;
2 # ABSTRACT: Reading and writing groupsecret keyfiles
6 use App::GroupSecret::File;
8 my $file = App::GroupSecret::File->new('path/to/keyfile.yml');
9 print "File version: " . $file->version, "\n";
11 $file->add_key('path/to/key_rsa.pub');
16 This module provides a programmatic way to manage keyfiles.
18 See L<groupsecret> for the command-line interface.
22 Keyfiles are YAML documents that contains this structure:
30 secret_passphrase: PASSPHRASE...
40 our $VERSION = '9999.999'; # VERSION
42 use App
::GroupSecret
::Crypt
qw(
43 generate_secure_random_bytes
44 read_openssh_public_key
45 read_openssh_key_fingerprint
53 use YAML
::Tiny
qw(LoadFile DumpFile);
56 our $FILE_VERSION = 1;
58 sub _croak
{ require Carp
; Carp
::croak
(@_) }
59 sub _usage
{ _croak
("Usage: @_\n") }
63 $file = App
::GroupSecret
::File-
>new($filepath);
65 Construct a new keyfile object
.
71 my $filepath = shift or _croak
(q{App::GroupSecret::File->new($filepath)});
72 return bless {filepath
=> $filepath}, $class;
77 Get the filepath of the keyfile
.
81 sub filepath
{ shift-
>{filepath
} }
87 Get a raw hashref with the contents of the keyfile
.
93 return $self->{info
} ||= do {
94 if (-e
$self->filepath) {
107 Get a hashref representing an empty keyfile
, used
for initializing a new keyfile
.
115 version
=> $FILE_VERSION,
122 $info = $file->load($filepath);
124 Load
(or reload
) the contents of a keyfile
.
130 my $filepath = shift || $self->filepath;
131 my $info = LoadFile
($filepath) || {};
133 $self->{info
} = $info if !$filepath;
140 $file->save($filepath);
142 Save the keyfile to disk
.
148 my $filepath = shift || $self->filepath;
149 DumpFile
($filepath, $self->info);
158 Check the file format of a keyfile to make sure this module can understand it
.
164 my $info = shift || $self->info;
166 _croak
'Corrupt file: Bad type for root' if !$info || ref $info ne 'HASH';
168 my $version = $info->{version
};
169 _croak
'Unknown file version' if !$version || $version !~ /^\d+$/;
170 _croak
'Unsupported file version' if $FILE_VERSION < $version;
172 _croak
'Corrupt file: Bad type for keys' if ref $info->{keys} ne 'HASH';
174 warn "The file has a secret but no keys to access it!\n" if $info->{secret
} && !%{$info->{keys}};
183 Get a hashref of the
keys from a keyfile
.
187 $secret = $file->secret;
189 Get the secret from a keyfile as an encrypted string
.
193 $version = $file->version
195 Get the file format version
.
199 sub keys { shift-
>info->{keys} }
200 sub secret
{ shift-
>info->{secret
} }
201 sub version
{ shift-
>info->{version
} }
205 $file->add_key($filepath);
207 Add a key to the keyfile
.
213 my $public_key = shift or _usage
(q{$file->add_key($public_key)});
214 my $args = @_ == 1 ? shift : {@_};
216 my $keys = $self->keys;
218 my $info = $args->{fingerprint_info
} || read_openssh_key_fingerprint
($public_key);
219 my $fingerprint = $info->{fingerprint
};
222 comment
=> $info->{comment
},
223 filename
=> basename
($public_key),
224 secret_passphrase
=> undef,
225 type
=> $info->{type
},
228 if ($args->{embed
}) {
229 open(my $fh, '<', $public_key) or die "open failed: $!";
230 $key->{content
} = do { local $/; <$fh> };
231 chomp $key->{content
};
234 $keys->{$fingerprint} = $key;
237 my $passphrase = $args->{passphrase
} || $self->decrypt_secret_passphrase($args->{private_key
});
238 my $ciphertext = encrypt_rsa
(\
$passphrase, $public_key);
239 $key->{secret_passphrase
} = $ciphertext;
242 return wantarray ? ($fingerprint => $key) : $key;
247 $file->delete_key($fingerprint);
249 Delete a key from the keyfile
.
255 my $fingerprint = shift;
256 delete $self->keys->{$fingerprint};
259 =method decrypt_secret
261 $secret = $file->decrypt_secret(passphrase
=> $passphrase);
262 $secret = $file->decrypt_secret(private_key
=> $private_key);
264 Get the decrypted secret
.
270 my $args = @_ == 1 ? shift : {@_};
272 $args->{passphrase
} || $args->{private_key
} or _usage
(q{$file->decrypt_secret($private_key)});
274 my $passphrase = $args->{passphrase
};
275 $passphrase = $self->decrypt_secret_passphrase($args->{private_key
}) if !$passphrase;
277 my $ciphertext = $self->secret;
278 return decrypt_aes_256_cbc
(\
$ciphertext, $passphrase);
281 =method decrypt_secret_passphrase
283 $passphrase = $file->decrypt_secret_passphrase($private_key);
285 Get the decrypted secret passphrase
.
289 sub decrypt_secret_passphrase
{
291 my $private_key = shift or _usage
(q{$file->decrypt_secret_passphrase($private_key)});
293 my $info = read_openssh_key_fingerprint
($private_key);
294 my $fingerprint = $info->{fingerprint
};
296 my $keys = $self->keys;
297 if (my $key = $keys->{$fingerprint}) {
298 return decrypt_rsa
(\
$key->{secret_passphrase
}, $private_key);
301 die "The private key ($private_key) is not able to decrypt the keyfile.\n";
304 =method encrypt_secret
306 $file->encrypt_secret($secret, $passphrase);
308 Set the secret by encrypting it with a
256-bit passphrase
.
310 Passphrase must be
32 bytes
.
316 my $secret = shift or _usage
(q{$file->encrypt_secret($secret)});
317 my $passphrase = shift or _usage
(q{$file->encrypt_secret($secret)});
319 my $ciphertext = encrypt_aes_256_cbc
($secret, $passphrase);
320 $self->info->{secret
} = $ciphertext;
323 =method encrypt_secret_passphrase
325 $file->encrypt_secret_passphrase($passphrase);
327 Set the passphrase by encrypting it with
each key
in the keyfile
.
331 sub encrypt_secret_passphrase
{
333 my $passphrase = shift or _usage
(q{$file->encrypt_secret_passphrase($passphrase)});
335 while (my ($fingerprint, $key) = each %{$self->keys}) {
336 local $key->{fingerprint
} = $fingerprint;
337 my $pubkey = $self->find_public_key($key) or die 'Cannot find public key: ' . $self->format_key($key) . "\n";
338 my $ciphertext = encrypt_rsa
(\
$passphrase, $pubkey);
339 $key->{secret_passphrase
} = $ciphertext;
343 =method find_public_key
345 $filepath = $file->find_public_key($key);
347 Get a path to the public key file
for a key
.
351 sub find_public_key
{
353 my $key = shift or _usage
(q{$file->find_public_key($key)});
355 if ($key->{content
}) {
356 my $temp = File
::Temp-
>new(UNLINK
=> 1);
357 print $temp $key->{content
};
359 $self->{"temp:$key->{fingerprint}"} = $temp;
360 return $temp->filename;
363 my @dirs = split(/:/, $ENV{GROUPSECRET_PATH
} || ".:keys:$ENV{HOME}/.ssh");
364 for my $dir (@dirs) {
365 my $filepath = File
::Spec-
>catfile($dir, $key->{filename
});
366 return $filepath if -f
$filepath;
373 $str = $file->format_key($key);
375 Get a one-line summary of a key
. Format
is "<fingerprint> <comment>".
381 my $key = shift or _usage
(q{$file->format_key($key)});
383 my $fingerprint = $key->{fingerprint
} or _croak
(q{Missing required field in key: fingerprint});
384 my $comment = $key->{comment
} || 'uncommented';
386 if ($fingerprint =~ /^[A-Fa-f0-9]{32}$/) {
387 $fingerprint = 'MD5:' . join(':', ($fingerprint =~ /../g ));
389 elsif ($fingerprint =~ /^[A-Za-z0-9\/\
+]{27}$/) {
390 $fingerprint = "SHA1:$fingerprint";
392 elsif ($fingerprint =~ /^[A-Za-z0-9\/\
+]{43}$/) {
393 $fingerprint = "SHA256:$fingerprint";
396 return "$fingerprint $comment";