Camelia

Benchmarking crypt with SHA-512 in Perl 6

Earlier I've explained how to encrypt asswords in Perl 6 using crypt and SHA-512. Some of the commenters have suggested that it can be further improved by adding a "round-factor" further increasing the computation time and thereby making it similar to PBKDF or scrypt.

In this article we'll see how and what is the impact of this extra work.

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

Encrypt with rounds

Apparently an (undocumented?) feature of crypt is that one can pass another field called rounds that instructs crypt to encrypt the already encrypted password several additional times. Thereby increasing the calculation time required to verify a password. When a regular user tries to log-in, this extra computation makes the process a bit slower but does not have a huge impact on the user. On the other hand if someone tries to figure out what could have been the password, this extra time will make it impossible to use brute force to find the right password. Even if the attacker has received a copy of the encrypted password.

The way to pass the number of rounds is by adding it to the prefix of the salt:

$6$rounds=ROUNDS$SALT

Where ROUNDS is the number of times the password has to be re-encrypted.

The following program demonstrates the use.

examples/encryp_with_rounds.pl

use v6;
use Crypt::Libcrypt; 

my $password = 'secret';

say encrypt(0);
say encrypt(1);
say encrypt(42);
say encrypt(1000);
say encrypt(50000);
say encrypt(200000);
say encrypt(1000000);

sub encrypt($rounds) {
    say "\n$rounds";
    my $salt = '$6$abcdefghIJKLMOPQ';
    if $rounds {
        $salt = '$6$rounds=' ~ $rounds ~ '$abcdefghIJKLMOPQ';
    }
    say $salt;
    my $start = now;
    my $encripted = crypt($password, $salt);
    my $end = now;
    say "Elapsed time: ", ($end-$start);
    return $encripted;
}

In addition to printing the encrypted password this program also prints the salt used and the time it too to encrypt the password:


0
$6$abcdefghIJKLMOPQ
Elapsed time: 0.04111855
$6$abcdefghIJKLMOPQ$k7/p0miPUkmsxrxWKDuP5Apnb6MZ6.vfG6xulRYIRCnRtDXXgf5rA7M3yb5TeUdg1I8Hxux6BDbsf.ZABF/re.

1
$6$rounds=1$abcdefghIJKLMOPQ
Elapsed time: 0.0017339
$6$rounds=1000$abcdefghIJKLMOPQ$OcJQW0dfxJ6UxjuOg86iAj9seVPfZ.3Kz9FQ3psUOpFEeKCZZ6ccrPKDUwSGa925S1ed8sJN1V8JbOcgAMehS/

42
$6$rounds=42$abcdefghIJKLMOPQ
Elapsed time: 0.0018392
$6$rounds=1000$abcdefghIJKLMOPQ$OcJQW0dfxJ6UxjuOg86iAj9seVPfZ.3Kz9FQ3psUOpFEeKCZZ6ccrPKDUwSGa925S1ed8sJN1V8JbOcgAMehS/

1000
$6$rounds=1000$abcdefghIJKLMOPQ
Elapsed time: 0.00167532
$6$rounds=1000$abcdefghIJKLMOPQ$OcJQW0dfxJ6UxjuOg86iAj9seVPfZ.3Kz9FQ3psUOpFEeKCZZ6ccrPKDUwSGa925S1ed8sJN1V8JbOcgAMehS/

50000
$6$rounds=50000$abcdefghIJKLMOPQ
Elapsed time: 0.0360235
$6$rounds=50000$abcdefghIJKLMOPQ$QmLwmcn0vf4dsVNj3f/iuZLfM8s6ddbzif6UsRFMMHoq1cpf.Aut4tfciwgsnllM3Eko/Tb8b8OUV59G/7qkq.

200000
$6$rounds=200000$abcdefghIJKLMOPQ
Elapsed time: 0.1321950
$6$rounds=200000$abcdefghIJKLMOPQ$HsRzx4rmtlGaD9ohLjeBLY09cOSJfSx2J42SJXrp5TxCkl7LQaBG85N.MBj0o7iHq1LxRRLRS.GQsxsyxLVfp.

1000000
$6$rounds=1000000$abcdefghIJKLMOPQ
Elapsed time: 0.6521933
$6$rounds=1000000$abcdefghIJKLMOPQ$RM5BrpnL219ylybDDHBYRbTPk1XvDOPjbzS6Wwc4qA/SlQLyUSFumxoRbAb8ZQvrfELanff.4FQfcPlb8kF5D.

Based on these numbers I can see that the minimal number of rounds is 1000. There is also something strange in the results as it seems to indicate that crypt without any rounds works about 50 times slower that crypt with 1000 rounds. (50,000 rounds seems to take about the same time.) This might need more investigation.

We can also see that the encrypted passwords differ.

At 1,000,000 rounds the waiting time can be really felt already.

It still seems acceptable though for the one-time password check when someone logs in.

Benchmarking using Benchmark

I found two Perl 6 modules for benchmarking. The older and simpler Benchmark module that has the nice advantage of returning the results instead of just printing them.

examples/benchmark_crypt.pl

use v6;

use Crypt::Libcrypt; 
use Benchmark;

my $password = 'secret';
my %results = timethese(100, {
	'none' => sub {
		my $encripted = crypt($password, '$6$abcdefghIJKLMOPQ');
	},
    '200000' => sub {
		my $encripted = crypt($password, '$6$rounds=200000$abcdefghIJKLMOPQ');
	},
    '1000000' => sub {
		my $encripted = crypt($password, '$6$rounds=1000000$abcdefghIJKLMOPQ');
	},
});
say ~%results;

The result, (after some manual aligning of the data):

1000000 1492331790 1492331854 64 0.64
200000  1492331854 1492331867 13 0.13
none    1492331854 1492331854 0  0

The values in the result line are the following:

NAME    START-time      END-time    Elapsed-time  Average time

From this we can already see that 1,000,000 rounds is roughly 5 times slower than 200,000 which is the expected number and that they are both unmeasurably slower that no-rounds.

Actually the big drawback of this module is that currently it used the time function that measures only full seconds while it could use the now function that returns a high-resolution timestamp. I patched it and got this result (after some manual layout cosmetics):

1000000 1492334514.729730   1492334586.011102   71.281372     0.712813718928151
200000  1492334586.013800   1492334599.485665   13.47186421   0.134718642148155
none    1492334599.486490   1492334599.846348   0.35985759    0.00359857592694458

This time it is much clearer that the 200,000 rounds version is about 40 times slower than the no-round version.

Benchmarking using Bench

Another module for benchmariking is the newer Bench module.

examples/bench_crypt.pl

use v6;
use Crypt::Libcrypt; 
use Bench;

my $password = 'secret';
my $b = Bench.new;

$b.timethese(100, {
    'none' => sub {
        my $encripted = crypt($password, '$6$abcdefghIJKLMOPQ');
    },
    '200000' => sub {
        my $encripted = crypt($password, '$6$rounds=200000$abcdefghIJKLMOPQ');
    },
    '1000000' => sub {
        my $encripted = crypt($password, '$6$rounds=1000000$abcdefghIJKLMOPQ');
    },
});



The result looks like this:

Benchmark:
Timing 100 iterations of 1000000, 200000, none...
   1000000: 65.9749 wallclock secs @ 1.5157/s (n=100)
    200000: 12.9219 wallclock secs @ 7.7388/s (n=100)
      none: 0.3153 wallclock secs @ 317.1891/s (n=100)
            (warning: too few iterations for a reliable count)

More iterations for the benchmark

In order to provide a more accurate measurement we should probably use a higher iteration count than 100, but at a 1,000,000 rounds that would become really slow. (That's what we really want, but it is uncomfortable when benchmarking.)

Conclusion

The Benchmark modules seem to work. Having a round of 200,000 already has a 40-fold impact, but as far as I know the recommendation is to use the highest round-number that is still usable.


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-04-16



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.6295691

Perl 6 Tricks and Treats newsletter

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