File Coverage

blib/lib/Crypt/Age.pm
Criterion Covered Total %
statement 71 71 100.0
branch 9 16 56.2
condition 13 30 43.3
subroutine 11 11 100.0
pod 5 5 100.0
total 109 133 81.9


line stmt bran cond sub pod time code
1             package Crypt::Age;
2             our $AUTHORITY = 'cpan:GETTY';
3             # ABSTRACT: Perl implementation of age encryption (age-encryption.org)
4              
5 3     3   451277 use Moo;
  3         36463  
  3         15  
6 3     3   9108 use Carp qw(croak);
  3         6  
  3         185  
7 3     3   1744 use Crypt::Age::Keys;
  3         10  
  3         128  
8 3     3   1678 use Crypt::Age::Primitives;
  3         10  
  3         136  
9 3     3   1691 use Crypt::Age::Header;
  3         13  
  3         141  
10 3     3   29 use namespace::clean;
  3         6  
  3         16  
11              
12              
13             our $VERSION = '0.001';
14              
15             sub generate_keypair {
16 9     9 1 394279 my ($class) = @_;
17 9         56 return Crypt::Age::Keys->generate_keypair;
18             }
19              
20              
21             sub encrypt {
22 10     10 1 3336 my ($class, %args) = @_;
23 10   66     328 my $plaintext = $args{plaintext} // croak "plaintext required";
24 9   66     182 my $recipients = $args{recipients} // croak "recipients required";
25              
26 8 50       31 croak "recipients must be an array ref" unless ref($recipients) eq 'ARRAY';
27 8 100       189 croak "at least one recipient required" unless @$recipients;
28              
29             # Generate random file key
30 7         40 my $file_key = Crypt::Age::Primitives->generate_file_key;
31              
32             # Create header with wrapped file key for each recipient
33 7         228 my $header = Crypt::Age::Header->create($file_key, $recipients);
34              
35             # Generate payload nonce and derive payload key
36 7         26 my $nonce = Crypt::Age::Primitives->generate_payload_nonce;
37 7         159 my $payload_key = Crypt::Age::Primitives->derive_payload_key($file_key, $nonce);
38 7         25 my $encrypted_payload = Crypt::Age::Primitives->encrypt_payload($payload_key, $plaintext);
39              
40             # Output: header + nonce + encrypted_payload
41 7         31 return $header->to_string . $nonce . $encrypted_payload;
42             }
43              
44              
45             sub decrypt {
46 9     9 1 2735 my ($class, %args) = @_;
47 9   66     221 my $ciphertext = $args{ciphertext} // croak "ciphertext required";
48 8   33     23 my $identities = $args{identities} // croak "identities required";
49              
50 8 50       28 croak "identities must be an array ref" unless ref($identities) eq 'ARRAY';
51 8 50       18 croak "at least one identity required" unless @$identities;
52              
53             # Parse header
54 8         9 my $offset = 0;
55 8         35 my $header = Crypt::Age::Header->parse(\$ciphertext, \$offset);
56              
57             # Unwrap file key using identities
58 8         144 my $file_key = $header->unwrap_file_key($identities);
59              
60             # Extract nonce (first 16 bytes after header) and encrypted payload
61 7         19 my $nonce = substr($ciphertext, $offset, 16);
62 7         376 my $encrypted_payload = substr($ciphertext, $offset + 16);
63              
64             # Derive payload key using nonce
65 7         29 my $payload_key = Crypt::Age::Primitives->derive_payload_key($file_key, $nonce);
66              
67 7         27 return Crypt::Age::Primitives->decrypt_payload($payload_key, $encrypted_payload);
68             }
69              
70              
71             sub encrypt_file {
72 1     1 1 2405 my ($class, %args) = @_;
73 1   33     7 my $input = $args{input} // croak "input required";
74 1   33     6 my $output = $args{output} // croak "output required";
75 1   33     5 my $recipients = $args{recipients} // croak "recipients required";
76              
77 1 50       52 open my $in_fh, '<:raw', $input
78             or croak "Cannot open input file '$input': $!";
79 1         4 my $plaintext = do { local $/; <$in_fh> };
  1         6  
  1         33  
80 1         15 close $in_fh;
81              
82 1         11 my $ciphertext = $class->encrypt(
83             plaintext => $plaintext,
84             recipients => $recipients,
85             );
86              
87 1 50       153 open my $out_fh, '>:raw', $output
88             or croak "Cannot open output file '$output': $!";
89 1         9 print $out_fh $ciphertext;
90 1         267 close $out_fh;
91              
92 1         20 return 1;
93             }
94              
95              
96             sub decrypt_file {
97 1     1 1 678 my ($class, %args) = @_;
98 1   33     7 my $input = $args{input} // croak "input required";
99 1   33     4 my $output = $args{output} // croak "output required";
100 1   33     3 my $identities = $args{identities} // croak "identities required";
101              
102 1 50       57 open my $in_fh, '<:raw', $input
103             or croak "Cannot open input file '$input': $!";
104 1         4 my $ciphertext = do { local $/; <$in_fh> };
  1         6  
  1         33  
105 1         16 close $in_fh;
106              
107 1         9 my $plaintext = $class->decrypt(
108             ciphertext => $ciphertext,
109             identities => $identities,
110             );
111              
112 1 50       113 open my $out_fh, '>:raw', $output
113             or croak "Cannot open output file '$output': $!";
114 1         7 print $out_fh $plaintext;
115 1         192 close $out_fh;
116              
117 1         16 return 1;
118             }
119              
120              
121              
122             1;
123              
124             __END__