Camelia

Encrypting Passwords in Perl 6 using crypt

There are several Perl 6 modules to provide one-way encryption of passwords.

I think Crypt::Bcrypt or Crypt::Argon2 would be the recommended ones to use, but currently they are both broken due to a dependency being broken. In the meantime, let's explore how the encryption could work using Crypt::Libcrypt.

Actually even Libcrypt can be enhanced by using SHA-512. This improvement is described in the new article.

Note! This site is about Perl 6.
If you are looking for a solution for Perl 5, please check out the Perl 5 tutorial.

Crypt::Libcrypt exports a single funcion called crypt that takes 2 parameters. A password in clear-text and a "salt". A "salt" is just a string that is used to protect against dictionary attacks using Rainbow tables.

In the case of the crypt function it must be a random 2-character string.

Encrypt password

In the first example we have a password in a variable and a fixed salt:

examples/libcrypt_encrypt_password.pl

use v6;
use Crypt::Libcrypt; 

my $password = "secret";
my $salt = "ab";

my $encripted = crypt($password, $salt);
say $encripted;

The output of this script is:

abNANd1rDfiNc

Verify password

In order to verify the password we need to have access to the encrypted password and we can either use that string as the salt, (The encrypt function will actually use the first 2 chaacters only.) or we can use the substr method to extract the salt from the encrypted password and use that on the orignal password.

examples/libcrypt_verify_password.pl

use v6;
use Crypt::Libcrypt; 

my $password = "secret";

my $encripted = 'abNANd1rDfiNc';

if crypt($password, $encripted) eq $encripted {
    say "Verified";
}

my $salt = $encripted.substr(0, 2);
if crypt($password, $salt) eq $encripted {
    say "Verified";
}

In either case the result is expected to be identical to the already encrypted password.

Register and Authenticate

In a real application the algorithm might look like this: When you register on a web site (or add a new user) you need to provide something to identify you (e.g. a username) and something to later verify that it is the same person coming back again. For that we need a password.

(We might also ask for an e-mail or some other information, but for our purposes a username and a password will be enough.)

We then check if that identifyer (in this case the username) is free or if has already been used in our system. If it has not been used yet then we generate a random seed, encrypt the password and store the username and the encrupted password.

When at a later point you visit our site again we'll ask for your username and password again. At this time, we retreive the encrypted password associated to the given username from our database. If there was no such username we report that we could not identify you.

If we got the encrypted password then we use it as the salt to encrypt the password given to us just now. If the encryption results in the same string as the encrypted string we fetched from the database then we could verify that it is indeed the same username/password we got earlier.

Generate random salt

First let's see how to generate random salt:

examples/random_salt.pl

use v6;

my @chars = 'a' ... 'z', 'A' ... 'Z', '0' ... '9';
say @chars;
my $salt = @chars.pick(2).join('');
say $salt;

For this we created a list of potential characters and then use the pick method to pick two of the characters. Finally used join to turn that into a string.

Accepting passwords on the command line

Instead of building a web application, for this example we'll use the command line. For that we need to be able to accept passwords on the command line. We have already seen this in the getting started with Perl 6 on Docker article, but let's see the solution here again using Terminal::Readsecret:

examples/read_password.pl

use v6;
use Terminal::Readsecret;

my $password = getsecret("password:" );
say "your password is: " ~ $password;

Register and Authenticate

examples/users.pl

use v6;

use Terminal::Readsecret;
use Crypt::Libcrypt;

main();

sub main() {
    my $database = 'users.csv';

    my $r = prompt("Register or Authenicate? [R/A] ");
    if $r.lc eq 'r' {
        say "Register";
        my $username = prompt("Username:").lc;
        
        # TODO: check for validity of username
    
        my %users = read_users($database);
        # say %users;
        if %users{$username} {
            say "This user already exists";
            exit;
        }
    
        # check if that username already exists
        my $password = getsecret("password:");
        # save username/hashed password
        %users{$username} = crypt($password, get_salt());
        save_users($database, %users);
    } elsif $r.lc() eq 'a' {
        say "Authenticate";
        my $username = prompt("Username:");
        my $password = getsecret("password:");
        my %users = read_users($database);

        # fetch the hashed password of this user
        # check if that equals to the hashed version of this password
        if %users{$username} and crypt($password, %users{$username}) eq $%users{$username} {
            say "Welcome back!";
        } else {
            say "Invalid access";
        }

    } else {
        say "Bye";
    }
}

sub read_users($database) {
    # say "Read $database";
    my %users;
    if $database.IO.e {
        my @lines = $database.IO.lines();
        for @lines -> $line {
            my ($name, $pw) = $line.split(",");
            %users{$name} = $pw;
        }
    }
    return %users;
}

sub save_users($database, %users) {
    # say $database;
    my $fh = open($database, :w);
    LEAVE $fh.close;
    for %users.keys -> $k {
        $fh.print("$k,%users{$k}\n");
    }
}

sub get_salt() {
    my @chars = 'a' ... 'z', 'A' ... 'Z', '0' ... '9';
    return @chars.pick(2).join('');
}

Our "database" is a plain csv file with the username and encrypted password fields separated by a comma.

get_salt will return a random salt.

read_users reads the csv file, splits each record into username and password and stores them in a hash.

save_users will save the hash of username - (encrypted) password pairs in the "database".

The main function has two parts. One handling new users "registering" and the other handling existing users "authenticating". The algorithm is described above.

Conclusion

While using crypt is not recommened for real password storage due to its weak encryption, it can be used to demonstrate how the algorithm could be used.


The Perl 6 Tricks and Treats newsletter has been around for a while. If you are interested to get special notification when there is new content on this site, it is the best way to keep track:
Email:
Full name:
This is a newsletter temporarily running on my personal site (szabgab.com) using Mailman, till I implement an alternative system in Perl 6.
Gabor Szabo
Written by Gabor Szabo

Published on 2017-03-29



Comments

In the comments, please wrap your code snippets within <pre> </pre> tags and use spaces for indentation.
comments powered by Disqus
Suggest a change
Elapsed time: 2.97351996

Support my Perl 6 book!

I am writing a book on Web Application Development in Perl 6 and raising money via crowdfunding. Please support my effort!

Perl 6 Tricks and Treats newsletter

Register to the free newsletter now, and get updates and news.
Email:
Name: