File Coverage

blib/lib/Crypt/Credentials.pm
Criterion Covered Total %
statement 109 116 93.9
branch 23 38 60.5
condition 17 32 53.1
subroutine 21 23 91.3
pod 9 9 100.0
total 179 218 82.1


line stmt bran cond sub pod time code
1             package Crypt::Credentials;
2             $Crypt::Credentials::VERSION = '0.006';
3 2     2   285811 use strict;
  2         3  
  2         62  
4 2     2   7 use warnings;
  2         2  
  2         110  
5              
6 2     2   12 use Carp 'croak';
  2         3  
  2         123  
7 2     2   1005 use Crypt::AuthEnc::GCM qw/gcm_encrypt_authenticate gcm_decrypt_verify/;
  2         6264  
  2         128  
8 2     2   839 use Crypt::SysRandom 'random_bytes';
  2         6326  
  2         151  
9 2     2   15 use File::Basename 'dirname';
  2         3  
  2         109  
10 2     2   8 use File::Path 'make_path';
  2         2  
  2         82  
11 2     2   947 use File::Slurper qw/read_binary write_binary/;
  2         20270  
  2         136  
12 2     2   975 use File::Spec::Functions qw/catdir catfile curdir updir abs2rel rel2abs/;
  2         1705  
  2         195  
13 2     2   1214 use YAML::PP;
  2         147851  
  2         3217  
14              
15             sub new {
16 2     2 1 160212 my ($class, %args) = @_;
17              
18 2   33     18 my $dir = rel2abs($args{dir} // catdir(curdir, 'credentials'));
19              
20 2         38 my $check_file = catfile($dir, 'check.enc');
21              
22 2         5 my $real_key;
23              
24 2 100       71 if (-f $check_file) {
25 1         3 for my $key (@{ $args{keys} }) {
  1         4  
26 2         3 my $length = length $key;
27 2 0 33     7 croak "Invalid key size($length)" if $length != 16 && $length != 24 && $length != 32;
      33        
28 2   100     3 my $tag = eval { $class->_get($check_file, $key) } // '';
  2         6  
29 2 100       6 if ($tag eq 'OK') {
30 1         3 $real_key = $key;
31 1         4 last;
32             }
33             }
34             } else {
35 1         3 ($real_key) = @{ $args{keys} };
  1         4  
36 1         2 my $length = length $real_key;
37 1 0 33     5 croak "Invalid key size($length)" if $length != 16 && $length != 24 && $length != 32;
      33        
38 1         83 make_path($dir);
39 1         14 $class->_put($check_file, $real_key, 'OK');
40             }
41 2 50       204 croak 'No working key found' unless defined $real_key;
42              
43 2         23 return bless {
44             key => $real_key,
45             dir => $dir,
46             }, $class;
47             }
48              
49             my $ypp = YAML::PP->new;
50             my $format = 'a16 a16 a*';
51              
52             sub _put {
53 4     4   15 my ($self, $filename, $key, $plaintext) = @_;
54 4         28 my $iv = random_bytes(16);
55 4         984 my ($ciphertext, $tag) = gcm_encrypt_authenticate('AES', $key, $iv, '', $plaintext);
56 4         22 my $payload = pack $format, $iv, $tag, $ciphertext;
57 4         22 write_binary($filename, $payload);
58             }
59              
60             sub put {
61 1     1 1 5 my ($self, $name, $plaintext) = @_;
62 1         9 my $filename = catfile($self->{dir}, "$name.yml.enc");
63 1         70 my $dirname = dirname($filename);
64 1         66 make_path($dirname);
65 1         5 $self->_put($filename, $self->{key}, $plaintext);
66 1         234 return;
67             }
68              
69             sub put_yaml {
70 1     1 1 4 my ($self, $name, @content) = @_;
71 1         8 my $plaintext = $ypp->dump_string(@content);
72 1         3619 return $self->put($name, $plaintext);
73             }
74              
75             sub _get {
76 5     5   11 my ($self, $filename, $key) = @_;
77 5         17 my $raw = read_binary($filename);
78 5         353 my ($iv, $tag, $ciphertext) = unpack $format, $raw;
79 5         1015 my $plaintext = gcm_decrypt_verify('AES', $key, $iv, '', $ciphertext, $tag);
80 5 100       208 croak 'Could not decrypt credentials file' if not defined $plaintext;
81 4         13 return $plaintext;
82             }
83              
84             sub get {
85 2     2 1 6 my ($self, $name) = @_;
86 2         31 my $filename = catfile($self->{dir}, "$name.yml.enc");
87 2 50       42 croak "No such credentials '$name'" if not -f $filename;
88 2         20 return $self->_get($filename, $self->{key});
89             }
90              
91             sub get_yaml {
92 2     2 1 12 my ($self, $name) = @_;
93 2         9 my $plaintext = $self->get($name);
94 2         11 return $ypp->load_string($plaintext);
95             }
96              
97             sub has {
98 0     0 1 0 my ($self, $name) = @_;
99              
100 0         0 return -f catfile($self->{dir}, "$name.yml.enc");
101             }
102              
103             sub _recode_dir {
104 1     1   3 my ($self, $dir, $new_key) = @_;
105              
106 1 50       21 opendir my $dh, $dir or croak "Could not open dir: $!";
107 1         18 while (my $file = readdir $dh) {
108 4 100 100     16 next if $file eq curdir || $file eq updir;
109 2         10 my $filename = catfile($dir, $file);
110              
111 2 50       25 if (-d $filename) {
    50          
112 0         0 $self->_recode_dir($filename, $new_key);
113             } elsif (-f $filename) {
114 2 100       9 next unless $file =~ /\.yml\.enc$/;
115 1         3 my $plaintext = $self->_get($filename, $self->{key});
116 1         8 $self->_put($filename, $new_key, $plaintext);
117             }
118             }
119             }
120              
121             sub recode {
122 1     1 1 106 my ($self, $new_key) = @_;
123              
124 1         3 my $key_length = length $new_key;
125 1 0 33     8 croak "Invalid key size($key_length)" if $key_length != 16 && $key_length != 24 && $key_length != 32;
      33        
126              
127 1         5 $self->_recode_dir($self->{dir}, $new_key);
128              
129 1         270 my $check_file = catfile($self->{dir}, 'check.enc');
130 1         6 $self->_put($check_file, $new_key, 'OK');
131 1         164 $self->{key} = $new_key;
132              
133 1         4 return;
134             }
135              
136             sub remove {
137 0     0 1 0 my ($self, $name) = @_;
138 0         0 my $filename = catfile($self->{dir}, "$name.yml.enc");
139 0         0 return unlink($filename);
140             }
141              
142             sub _list_dir {
143 2     2   7 my ($self, $base, $dir) = @_;
144 2 50       64 opendir my $dh, $dir or croak "No such dir $dir: $!";
145 2         8 my @files;
146 2         55 while (my $file = readdir $dh) {
147 7 100 100     34 next if $file eq curdir || $file eq updir;
148 3         15 my $filename = catfile($dir, $file);
149              
150 3 50 66     79 if (-d $filename) {
    100          
151 0         0 push @files, $self->_list_dir($base, $filename);
152             } elsif (-f $filename and $filename =~ s/\.yml\.enc$//) {
153 1         8 push @files, abs2rel($filename, $base);
154             }
155             }
156 2         155 return @files;
157             }
158              
159             sub list {
160 2     2 1 2608 my ($self, $base) = @_;
161 2 50       12 my $dir = $base ? catdir($self->{dir}, $base) : $self->{dir};
162 2 50       34 return if not -d $dir;
163              
164 2         10 return $self->_list_dir($self->{dir}, $dir);
165             }
166              
167             1;
168              
169             # ABSTRACT: Manage credential files
170              
171             __END__