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.
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.
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
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.
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.
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.
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;
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.
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.
Published on 2017-03-29