Argon2 constructs reference vectors

Since libsodium implements a different API for argon2 contructs than the one exposed by the reference implementation available at The password hash Argon2… <https://github.com/P-H-C/phc-winner-argon2/>, the kats data provided along to the reference implementation sources cannot be directly used as test vectors in PyNaCl tests.

Therefore, we are using a python driver for the command line argon2, which can be built following the instruction in the reference implementation sources.

Vector generation

The argondriver.py requires setting, via the command line option -x, the path to the argon2 executable; and as a default generates hex-encoded raw hash data on standard output.

Setting the -e option on the command line allows generating modular crypt formatted hashes.

The other command line options influence the minimum and maximum sizes of generated parameters as shown in the driver’s command line help, which is printed by inserting the -h option in the command line.

To generate vector data files in tests/data, the argondriver.py have been called to generate password hashes with parameters compatible with libsodium’s implementation; in particular, the minimum operations count must be 3 for argon2i and 1 for argon2id, and the salt length must be 16 for raw hashes, and can vary for modular crypt formatted hashes.

The full command lines used in generating the vactors are:

for raw argon2i
python3 docs/vectors/python/argondriver.py \
             -x ~/phc-winner-argon2/argon2 \
             -c argon2i \
             -s 16 -S 16 -p 8 -P 16 -m 14 -M 18 \
             -l 18 -L 32 -t 3 -T 5 -n 10 \
             -w  tests/data/raw_argon2id_hashes.json
for raw argon2id
python3 docs/vectors/python/argondriver.py \
             -x ~/phc-winner-argon2/argon2 \
             -c argon2id \
             -s 16 -S 16 -p 8 -P 16 -m 14 -M 18 \
             -l 18 -L 32 -t 1 -T 5 -n 10 \
             -w  tests/data/raw_argon2id_hashes.json
for modular crypt argon2i
python3 docs/vectors/python/argondriver.py \
             -x ~/phc-winner-argon2/argon2 \
             -c argon2i -e \
             -s 8 -S 16 -p 8 -P 16 -m 14 -M 18 \
             -l 18 -L 32 -t 3 -T 5  -n 10 \
             -w  tests/data/modular_crypt_argon2id_hashes.json
for modular crypt argon2id
python3 docs/vectors/python/argondriver.py \
             -x ~/phc-winner-argon2/argon2 \
             -c argon2id -e \
             -s 8 -S 16 -p 8 -P 16 -m 14 -M 18 \
             -l 18 -L 32 -t 1 -T 5  -n 10 \
             -w  tests/data/modular_crypt_argon2id_hashes.json

Code for the vector generator driver

The code for argondriver.py is available inside the docs/vectors/python directory of PyNaCl distribution and can also be directly downloaded from argondriver.py.

argondriver.py
#!/usr/bin/python
#
from __future__ import division, print_function

import argparse
import json
import random
import string
import subprocess
import sys


class argonRunner(object):
    GOODCHARS = string.ascii_letters + string.digits

    def __init__(self, args):
        self.exe = args.exe
        self.mnsaltlen = args.mnsaltlen
        self.mnpwlen = args.mnpwlen
        self.mndgstlen = args.mndgstlen
        self.mnmem = args.mnmem
        self.mniters = args.mniters
        self.mxsaltlen = args.mxsaltlen
        self.mxpwlen = args.mxpwlen
        self.mxdgstlen = args.mxdgstlen
        self.mxmem = args.mxmem
        self.mxiters = args.mxiters
        self.encoded = args.encoded
        self.rng = random.SystemRandom()
        self.version = args.version
        self.construct = args.construct
        self.maxcount = args.n
        self.count = 0

    def _runOnce(self, passwd, salt, dgst_len, maxmem, iters):
        """
        """
        argv = [self.exe, salt.encode('ascii'),
                '-t', '{0:2d}'.format(iters),
                '-m', '{0:2d}'.format(maxmem),
                '-l', '{0:3d}'.format(dgst_len),
                '-v', self.version,
                ]

        if self.encoded:
            argv.append('-e')
            mode = 'crypt'
        else:
            argv.append('-r')
            mode = 'raw'
        if self.construct == 'argon2i':
            argv.append('-i')
        elif self.construct == 'argon2d':
            argv.append('-d')
        elif self.construct == 'argon2id':
            argv.append('-id')
        p = subprocess.Popen(argv, stdin=subprocess.PIPE,
                             stdout=subprocess.PIPE)
        out, err = p.communicate(passwd.encode('ascii'))
        return dict(passwd=passwd, salt=salt, dgst_len=dgst_len,
                    maxmem=2 ** maxmem, iters=iters, mode=mode,
                    pwhash=out.decode('ascii').rstrip(),
                    construct=self.construct,
                    )

    def _genSalt(self):
        sltln = self.rng.randint(self.mnsaltlen, self.mxsaltlen)
        chrs = [self.rng.choice(self.GOODCHARS) for x in range(sltln)]
        return ''.join(chrs)

    def _genPw(self):
        pwln = self.rng.randint(self.mnpwlen, self.mxpwlen)
        chrs = [self.rng.choice(self.GOODCHARS) for x in range(pwln)]
        return ''.join(chrs)

    def __next__(self):
        if self.count >= self.maxcount:
            raise StopIteration
        psw = self._genPw()
        slt = self._genSalt()
        mem = self.rng.randint(self.mnmem, self.mxmem)
        iters = self.rng.randint(self.mniters, self.mxiters)
        dgstln = self.rng.randint(self.mndgstlen, self.mxdgstlen)
        rs = self._runOnce(psw, slt, dgstln, mem, iters)
        self.count += 1
        return rs

    def __iter__(self):
        return self

    next = __next__


if __name__ == '__main__':

    p = argparse.ArgumentParser()
    p.add_argument('-x', '--executable', dest='exe', required=True)
    p.add_argument('-c', '--construction', dest='construct',
                   type=str, default='argon2i')
    p.add_argument('-v', '--version', dest='version',
                   type=str, default='13')
    p.add_argument('-e', '--encoded', dest='encoded', default=False,
                   action='store_true',)
    p.add_argument('-s', '--min-salt-len', dest='mnsaltlen', type=int,
                   default=8)
    p.add_argument('-S', '--max-salt-len', dest='mxsaltlen', type=int,
                   default=8)
    p.add_argument('-p', '--min-password-len', dest='mnpwlen',
                   type=int, default=16)
    p.add_argument('-P', '--max-password-len', dest='mxpwlen',
                   type=int, default=16)
    p.add_argument('-l', '--min-digest-len', dest='mndgstlen',
                   type=int, default=64)
    p.add_argument('-L', '--max-digest-len', dest='mxdgstlen',
                   type=int, default=64)
    p.add_argument('-m', '--min-memory-exponent', dest='mnmem',
                   type=int, default=16)
    p.add_argument('-M', '--max-memory-exponent', dest='mxmem',
                   type=int, default=16)
    p.add_argument('-t', '--min-time-opscount', dest='mniters',
                   type=int, default=3)
    p.add_argument('-T', '--max-time-opscount', dest='mxiters',
                   type=int, default=3)
    p.add_argument('-n', '--count', dest='n', type=int, default=10)
    p.add_argument('-w', '--output', dest='outfile',
                   default=sys.stdout, type=argparse.FileType('w'))

    args = p.parse_args()

    res = [x for x in argonRunner(args)]

    json.dump(res, args.outfile, indent=2, separators=(',', ': '))