NAME

Vigil::Crypt - Encryption and Hashing wrapper for ChaCha20-Poly1305 and Argon2

SYNOPSIS

Encryption/Decryption

use Vigil::Crypt;

my $crypt = Vigil::Crypt->new( ENCRYPTION_KEY );

my $encrypted = $crypt->encrypt($plaintext_to_encrypt, $secret1, $secret2);

my $decrypted = $crypt->decrypt($encrypted, $secret1, $secret2);

Hashing

use Vigil::Crypt;

my $crypt = Vigil::Crypt->new( ENCRYPTION_KEY );

my $stored_hash = $crypt->hash($password, $PEPPER);

if( $crypt->verify_password($entered_password, $stored_hash, $PEPPER) ) {
	... passwords match ...
} else {
	... passwords did not match. Do not pass go, do not collect $200 ...
}

DESCRIPTION

Encryption/Decryption

The encrypt_data and decrypt_data methods handle sensitive information - like emails or personal IDs - in a way that makes it extremely difficult for anyone without the right keys to access. They use ChaCha20-Poly1305, a modern authenticated encryption algorithm trusted in security-critical applications worldwide. Not only does this algorithm scramble your data (encryption), it also includes a tag to ensure the data hasn't been tampered with (authentication). The system also derives an additional authenticated data (AAD) from user-specific information, which means even if someone guesses part of the input, they still cannot decrypt the data without the full context. For a newcomer, think of it as a strong lock on your secrets that refuses to open if anything looks suspicious - all handled automatically for you.

ChaCha20-Poly1305 is used for encryption/decryption because:

    * Provides authenticated encryption, ensuring data confidentiality and integrity.

    * Resistant to tampering: modifying the ciphertext or tag causes decryption to fail completely.

    * Fast and efficient in software, especially on CPUs without dedicated AES hardware.

    * Uses a 256-bit key, providing strong security against brute-force attacks.

    * Includes a nonce to ensure the same plaintext encrypts differently each time.

    * Supports Additional Authenticated Data (AAD), allowing context-specific data to be protected without including it in the ciphertext.

    * Widely standardized and recommended for modern secure communications.

    * Constantly reviewed and trusted by the cryptography community.

Hashing Passwords/Markers

Hashing is used for one-way protection, typically for passwords, but also for any verification markers or tokens that need to be validated without storing the original value. With hash and verify_password, sensitive data is never stored directly - only the hashed version. The module uses Argon2id, a state-of-the-art password hashing function designed to resist brute-force attacks while being memory- and CPU-intensive enough to slow down attackers. We also pre-hash the input with a pepper to add an extra layer of security. verify_password (or verify_hash) lets you safely check credentials or other verification markers: it confirms the input matches the stored hash without ever exposing the original value. In short, it's like a magic fingerprint - you can confirm it, but nobody can reverse-engineer it.

Argon2 is used for password hashing because:

    * It requires a large amount of RAM to compute.

    * GPU/ASIC attackers can't easily parallelize attacks because they run out of memory bandwidth.

    * This makes brute-force attacks extremely expensive on specialized hardware.

    * Resistant to Time-Memory Trade-Off (TMTO) attacks: attackers can't just reduce memory drastically without paying a huge time penalty.

    * Built with side-channel resistance in mind.

    * Future-proof against most foreseeable optimizations in hardware cracking.

    * Included in RFC 9106 as the recommended password hashing scheme.

    * Supported in libraries across most languages.

    * Constantly reviewed by cryptography experts.

CLASS METHODS

$obj->new(encryption_key => ENCRYPTION_KEY);

The constructor takes one argument and it is mandatory.

        use Vigil::Crypt;
        my $crypt = Vigil::Crypt->new( ENCRYPTION_KEY );
	

The encryption key must be a 64-character string of hexadecimal digits, which corresponds to 32 bytes of binary data. This value must be stored somewhere permanent and should never change; if it does, all previously encrypted items would need to be re-encrypted. The recommended approach is to store the key in a configuration file above web-root level, which your program can then require to access the key.

Here is a small script that you can run one time to generate a cryptographically secure encryption key:

        use strict;
        use warnings;
        use Bytes::Random::Secure qw(random_bytes);
        use MIME::Base16 qw(encode_base16);

        # Generate 32 random bytes
        my $key_bytes = random_bytes(32);

        # Convert to hex string (64 characters)
        my $key_hex = encode_base16($key_bytes);

        print "Random 32-byte key (hex): $key_hex\n";
        exit;
	

OBJECT METHODS

Encryption/Decryption

$obj->last_error;
        print $obj->last_error;
	

If your attempts to encrypt or decrypt a value fail, then you can print out the contents of this method to see why.

$obj->encrypt($value_to_encrypt, $user_specific_value_1, $user_specific_value2);
        my $encrypted_data = $obj->encrypt($plaintext, $userid, $user_account_date);
	

In my time developing things for the web (25+ years now), I've almost always used encryption with sensitive user information. This means that there has always been a user profile associated to the encrypted data. For this reason, I designed the encryption/decryption that could come directly from a user profile.

In my case, every profile has an ID number (userid), and every profile will include a date that it was created (user_account_date). Those two pieces of information will never (should never) change for an account profile. So if I encrypt something today, those two pieces of information should still be identical ten years from now.

The upside to this is that if the database table gets stolen, the bad-hat would only have the userid, they would not have the account creation date.

The second upside to this is that if the bad-hat did decrypt the data for one user, that decryption would not work with any other user, as all user's profile ids and dates would be different.

userid

Generally a sequence of digits, but it does not need to be. It just has to be unique to a user.

user_account_date

This can be in any format: ymd, timestamp, seconds since epoch, etc.

In fact, you can use any kind of data you want for these two values, so long as those pieces of data are unique to the user.

        my $encrypted_data = $obj->encrypt($plaintext, $aad);
	

If you have your own AAD, then you can pass that as a single argument.

AAD (Additional Authenticated Data) - Think of it like a label on a locked box. The box (your encrypted data) can't be opened unless the label matches exactly what it was when the box was locked. It's extra info used to verify the data hasn't been tampered with, without being hidden inside the box itself. If you don't really know what AAD is or how to use it, stick with the two pieces of user information as arguments.

my $encrypted_data = $obj->encrypt($plaintext);

You CAN do this but you SHOULD NOT do this. When you pass the data to encrypt with no further arguments, the module will generate it's own AAD for that encryption. It will be the same AAD generated for anyone else, though, so you really do compromise the security you are trying to enable with encryption.

$obj->decrypt($encrypted_data, $user_specific_value_1, $user_specific_value2);
my $plaintext = $obj->decrypt($encrypted_data, $userid, $user_account_date);

In this method, you are returning the encrypted text to plain text. The rules on arguments are the same as well. Remember that how you encrypt data (arguments supplied) must be the exact same way you decrypt data (arguments supplied).

Hashing

$obj->hash($password, $pepper);

IMPORTANT: The hash method produces a 32-byte binary value. When creating your database column to store this value, define it as: BINARY(32).

The pepper is optional but strongly recommended. It is an additional secret mixed into the password before hashing, which makes brute-force attacks significantly harder. Because verification requires the same pepper used during hashing, it must be stored securely, separate from the script - ideally in a configuration file above your web root or in a secure environment variable. You can generate a pepper in the same way you generate your encryption key.

In short, the difference between an encryption key and a pepper is: the encryption key is used to encrypt and decrypt data, while the pepper is used to strengthen password hashes.

$obj->verify_password($input_pwd, $stored_hashed_pwd, $pepper);
        if($obj->verify_password($input_pwd, $stored_hashed_pwd, $pepper)) {
            ...password challenge was A-OK, do your stuff...
        } else {
            ...password challenge failed, go away!...
        }
	

You need to pass the password being valided, then the password that was hashed and stored previously, and the pepper. Remember that the pepper for the original password and the validation method must be identical.

NOTE: This method can also be accessed as $obj->verify_hash($compare_hash, $stored_hash, $pepper);

Don't limit hashing to passwords. It can be used for one-way tokens or other verification markers. Let your imagination run rampant!

ENCRYPTION LENGTHS

Knowing how long the final encryption will be is important when designing your database tables. The formula to calculate the length of encrypted values is:

my $Base64_length = 4 * ceil(($plaintext_bytes + 28) / 3);

Since Perl does not have a ceil() function, we would actually calculate it this way:

my $Base64_length = 4 * int((($plaintext_bytes + 28) + 2) / 3);

Here are some prepresentative values to get you going:

Plaintext   Encrypted Base64
 (bytes)      (Characters)
16          4 * ceil((16 + 28)/3)   = 60
32          4 * ceil((32 + 28)/3)   = 80
64          4 * ceil((64 + 28)/3)   = 123
100         4 * ceil((100 + 28)/3)  = 172
256         4 * ceil((256 + 28)/3)  = 372
512         4 * ceil((512 + 28)/3)  = 688
1000        4 * ceil((1000 + 28)/3) = 1376
2500        4 * ceil((2500 + 28)/3) = 3352
5000        4 * ceil((5000 + 28)/3) = 6712

Local Installation

If your host does not allow you to install from CPAN, then you can install this module locally two ways:

  • Same Directory

    In the same directory as your script, create a subdirectory called "Vigil". Then add these two lines, in this order, to your script:

            use lib '.';           # Add current directory to @INC
            use Vigil::Crypt;      # Now Perl can find the module in the same dir
    	
            #Then call it as normal:
            my $crypt = Vigil::Crypt->new( ENCRYPTION_KEY );
  • In a different directory

    First, create a subdirectory called "Vigil" then add it to @INC array through a BEGIN{} block in your script:

            #!/usr/bin/perl
            BEGIN {
                push(@INC, '/path/on/server/to/Vigil');
            }
    	
            use Vigil::Crypt;
    	
            #Then call it as normal:
            my $crypt = Vigil::Crypt->new( ENCRYPTION_KEY );

AUTHOR

Jim Melanson (jmelanson1965@gmail.com).

Created: October, 2018.

Last Update: August 2025.

LICENSE

(Licence)

This module is free software; you may redistribute it and/or modify it under the same terms as Perl itself.