File Coverage

blib/lib/Vigil/Crypt.pm
Criterion Covered Total %
statement 66 83 79.5
branch 8 16 50.0
condition 10 28 35.7
subroutine 12 14 85.7
pod 6 7 85.7
total 102 148 68.9


line stmt bran cond sub pod time code
1             package Vigil::Crypt;
2            
3 2     2   444773 use strict;
  2         4  
  2         132  
4 2     2   19 use warnings;
  2         4  
  2         175  
5            
6 2     2   2536 use Bytes::Random::Secure qw(random_bytes);
  2         31695  
  2         216  
7 2     2   1672 use Crypt::AuthEnc::ChaCha20Poly1305;
  2         11108  
  2         157  
8 2     2   2305 use Digest::SHA qw(sha256);
  2         8801  
  2         246  
9 2     2   1393 use Crypt::Argon2 qw(argon2id_pass argon2_verify);
  2         2916  
  2         2258  
10            
11             our $VERSION = '2.1.2';
12            
13             sub new {
14 1     1 1 226732 my ($class, $encryption_key) = @_;
15 1 50 33     16 unless(defined $encryption_key && length($encryption_key) == 64) {
16 0         0 warn 'Vigil::Crypt was given an invalid encryption key.';
17 0         0 return;
18             }
19 1         11 bless { _encryption_key => pack("H*", $encryption_key), _last_error => '' }, $class;
20             }
21            
22 0   0 0 1 0 sub last_error { return $_[0]->{_last_error} // ''; }
23            
24             # ----------------------
25             # Encrypt a plaintext string
26             # ----------------------
27             sub encrypt {
28 1     1 1 13 my ($self, $plaintext, $aad1, $aad2) = @_;
29            
30 1         6 $self->{_last_error} = '';
31            
32             # validate plaintext
33 1 50 33     7 unless (defined $plaintext && length $plaintext) {
34 0         0 $self->{_last_error} = "missing plaintext";
35 0         0 return;
36             }
37            
38 1         5 my $aad = $self->_derive_key($aad1, $aad2);
39            
40             # optional: if _derive_key returns undef, just use empty string
41 1   50     4 $aad //= '';
42            
43 1         6 my $nonce = random_bytes(12);
44            
45 1         711 my $blob;
46             eval {
47 1         1216 my $cipher = Crypt::AuthEnc::ChaCha20Poly1305->new($self->{_encryption_key}, $nonce);
48 1         41 $cipher->adata_add($aad);
49 1         17 my $ct = $cipher->encrypt_add($plaintext);
50 1         23 my $tag = $cipher->encrypt_done();
51 1         4 $blob = $nonce . $ct . $tag; # assign to outer $blob
52 1         32 1;
53 1 50       3 } or do {
54 0   0     0 my $err = $@ || 'Unknown encryption error';
55 0         0 $self->{_last_error} = "encrypt failed: $err";
56 0         0 return;
57             };
58            
59             # return hex string for storage
60 1         32 return unpack("H*", $blob);
61             }
62            
63             # ----------------------
64             # Decrypt a hex-encoded ciphertext string
65             # ----------------------
66             sub decrypt {
67 1     1 1 1460 my ($self, $blob_hex, $aad1, $aad2) = @_;
68            
69 1         4 $self->{_last_error} = '';
70            
71 1 50 33     10 unless (defined $blob_hex && length $blob_hex) {
72 0         0 $self->{_last_error} = "missing blob";
73 0         0 return;
74             }
75            
76 1         3 my $decoded = eval { pack("H*", $blob_hex) };
  1         13  
77 1 50 33     7 if ($@ || !defined $decoded) {
78 0         0 $self->{_last_error} = "invalid hex blob";
79 0         0 return;
80             }
81            
82 1 50       4 if (length($decoded) < 28) {
83 0         0 $self->{_last_error} = "blob too short";
84 0         0 return;
85             }
86            
87 1         3 my $aad = $self->_derive_key($aad1, $aad2);
88 1   50     5 $aad //= '';
89            
90 1         3 my $nonce = substr($decoded, 0, 12);
91 1         2 my $tag = substr($decoded, -16); # last 16 bytes is the Poly1305 tag
92 1         4 my $ciphertext = substr($decoded, 12, -16); # between nonce and tag
93            
94 1         8 my $cipher = Crypt::AuthEnc::ChaCha20Poly1305->new($self->{_encryption_key}, $nonce);
95 1         5 $cipher->adata_add($aad);
96 1         37 my $plaintext = $cipher->decrypt_add($ciphertext);
97 1         22 my $ok = $cipher->decrypt_done($tag); # returns 1 if auth ok
98 1 50       4 unless ($ok) {
99 0         0 $self->{_last_error} = "decrypt failed: authentication failed";
100 0         0 return;
101             }
102 1         8 return $plaintext;
103             }
104            
105             # ----------------------
106             # Hashing functions
107             # ----------------------
108             sub hash {
109 1     1 1 805 my ($self, $password, $pepper) = @_;
110 1   50     5 $pepper //= '';
111 1         21 return argon2id_pass(
112             sha256($password . $pepper),
113             random_bytes(16),
114             3, '32M', 1, 32
115             );
116             }
117            
118             sub verify_hash {
119 1     1 0 151648 my ($self, $user_input, $stored_hash, $pepper) = @_;
120 1   50     4 $pepper //= '';
121 1         121930 return argon2_verify($stored_hash, sha256($user_input . $pepper));
122             }
123            
124 0     0 1 0 sub verify_password { return shift->verify_hash(@_); }
125            
126             sub _derive_key {
127 2     2   6 my ($self, $a, $b) = @_;
128 2   50     6 $a //= '';
129 2   50     13 $b //= '';
130 2         10 my $pepper = substr("1010" . $a, -4, 4) . substr($b . "101010101", 0, 9);
131 2 50       7 return undef unless length($pepper) == 13;
132 2         5 return $pepper;
133             }
134            
135             1;
136            
137            
138             __END__