Skip to main content

Idiomatic Argon2 password hashing for Python written in Rust

Project description

argonautica-py

Build Status Github.com License

Overview

argonautica is a Python package for hashing passwords that uses the cryptographically-secure argon2 hashing algorithm.

Argon2 won the Password Hashing Competition in 2015, a several year project to identify a successor to bcrypt, scrypt, and other common hashing algorithms.

argonautica was built with a simple use-case in mind: hashing passwords for storage in a website's database. That said, it's also "feature-complete", meaning anything you can do with the cannonical C implementation of argon2 you can do with argonautica*.

* Indeed, argonautica has a feature that even the cannonical C implementation lacks, i.e. hashing passwords with secret keys (the C implementation implements this, but does not expose it publicly)

Alternatives

There are several Python packages that implement argon2, including the excellent passlib, which uses argon2_cffi, but...

  • AFAIK, argonautica is the only Python implementation of argon2 that supports hashing with secret keys. Not even the cannonical C implementation of argon2 exposes this feature publicly (it's in the code, but unfortunately not accessable via the public API).

  • argonautica is the only Python implementation of argon2 to use SIMD instructions to peform it's hashing algorithm, which means it can be quite fast. The downside is that you have to compile it for your specific machine (this is why the pip install argonautica process takes time). That said, on the developer's early 2014 Macbook Air, which has SIMD instruction through AVX2, argonautica runs ~30% faster than passlib on default settings.

  • argonautica supports the latest argon2 variant: argon2id, which, unless you have a reason not to, you should be using. A number of Python implementations do not yet support this variant.

  • Finally, argonautica is the only Python implementation of argon2 written in Rust (as opposed to C or C++). Rust is a "systems programming language that runs blazingly fast, prevents segfaults, and guarantees thread safety."

Requirements

  • Python version 3.4 or higher (or PyPy version 3.5 or higher)
  • Rust version 1.26 or higher
  • LLVM version 3.9 or higher

Installation

  • Rust:
    • Follow the instructions here, which will just tell you to run the following command in your terminal and follow the on-screen instructions: curl https://sh.rustup.rs -sSf \| sh
  • LLVM:
    • macOS: brew install llvm, which requires Homebrew
    • Debian-based linux: apt-get install llvm-dev libclang-dev clang
    • Arch linux: pacman -S clang
    • Other linux: Use your distribution's package manager
    • Windows: Download a pre-built binary here
  • argonautica:
    • pip install --upgrade pip or pip install setuptools-rust. Note: setuptool-rust is not required if you have pip version 10.0 or above
    • pip install argonautica -v. Unfortunately, this step may take several minutes, as argonautica needs to compile it's Rust code for your specific CPU (due to its use of SIMD instructions). The upside, however, is that once compiled, argonautica should run blazingly fast

Usage

Hashing

from argonautica import Hasher

hasher = Hasher(secret_key='somesecret')
hasher.hash_len = 8
hasher.lanes = 2
hash = hasher.hash(password='P@ssw0rd')
print(hash)
# 👆 prints a random hash as the defeault `Hasher` uses a random salt by default

Verifying

from argonautica import Verifier

verifier = Verifier(secret_key='somesecret')
is_valid = verifier.verify(
    hash='$argon2id$v=19$m=4096,t=192,p=2$ULwasg5z5byOAork0UEhoTBVxIvAafKuceNz9NdCVXU$YxhaPnqRDys',
    password='P@ssw0rd',
)
assert(is_valid)

Configuration

from argonautica import Hasher, Verifier
from argonautica.config import Backend, Variant, Version

hasher = Hasher(secret_key=None)
# 👆 A secret key (passed as a keyword argument) is required to instantiate a
# Hasher, a Verifier, or an Argon2, but you are allowed to pass `None`
# in order to forgo using a secret key (this is not recommended)

hasher.additional_data = None  # Default is None
# 👆 Although rarely used, argon2 allows you to hash a password
# with not only salt and a secret key, but also with "additional data",
# which acts as a kind of secondary secret key. Like a secret key, it affects
# the outcome of the hash and is not stored in the string-encoded output, meaning
# later, to verify against a hash created with additional data, you will need to
# supply the same additional data manually to the Verifier (just like you have to
# do with a secret key). Again, this is rarely used.

hasher.backend = Backend.C  # Default is Backend.C
# 👆 argonautica was designed to support multiple backends (meaning multiple
# implementations of the underlying argon2 algorithm). Currently only the
# C backend is supported, which uses the cannonical argon2 library written
# in C to actually do the work. In the future a Rust backend will also be
# supported, but, for the moment, you must use Backend.C, which is the
# default. Using Backend.Rust will result in an error (again, for the
# moment).

hasher.hash_len = 32  # Default is 32
# 👆 The hash length in bytes is configurable. The default is 32.
# This is probably a good number to use. 16 is also probably fine.
# You probably shouldn't go below 16

hasher.iterations = 192  # Default is 192
# 👆 Argon2 has a notion of "iterations" or "time cost". All else equal
# and generally speaking, the greater the number of iterations, the
# longer it takes to perform the hash and the more secure the resulting
# hash. More iterations basically means more CPU load. This and "memory
# size" (see below) are the two primary parameters to adjust in order
# to increase or decrease the security of your hash. The default is
# 192 iterations, which was chosen because, along with the default
# memory size of 4096, this leads to a hashing time of approximately
# 300 milliseconds on the early-2014 Macbook Air that is the developer's
# machine. If you're going to use argonautica in production, you should
# probably tweak this parameter (and the memory size parameter) in order
# to increase the time it takes to hash to the maximum you can
# reasonably allow for your use-case (e.g. to probably about 300-500
# milliseconds for the use-case of hashing user passwords for a website)

hasher.lanes = 2  # Default is multiprocessing.cpu_count()
# 👆 Argon2 can break up its work into one or more "lanes" during some parts of
# the hashing algorithm. If you configure it with multiple lanes and you also
# use multiple threads (see below) the hashing algorithm will performed its
# work in parallel in some parts, potentially speeding up the time it takes to
# produce a hash without diminishing the security of the result. By default,
# the number of lanes is set to the number of logical cores on your machine

hasher.memory_size = 4096  # Default is 4096
# 👆 Argon2 has a notion of "memory size" or "memory cost" (in kibibytes). All else
# equal and generally speaking, the greater the memory size, the longer it takes to
# perform the hash and the more secure the resulting hash. More memory size basically
# means more memory used. This and "iterations" (see above) are, again, generally
# speaking, the two parameters to adjust in order to increase or decrease the
# security of your hash. The default is 4096 kibibytes, which was chosen because,
# again, along with the default iterations of 192, this leads to a hashing time of
# approximately 300 milliseconds on the early-2014 Macbook Air that is the
# developer's machine. If you're going to use argonautica in production, you should
# probably tweak this parameter (and the iterations parameter) in order to increase
# the time it takes to hash to the maximum you can reasonably allow for your use-case
# (e.g. to probably about 300-500 milliseconds for the use-case of hashing user
# passwords for a website)

hasher.threads = 2  # Default is multiprocessing.cpu_count()
# 👆 If you have configured a Hasher to use more than one lane (see above), you
# can get the hashing algorithm to run in parallel during some parts of the
# computation by setting the number of threads to be greater than one as well,
# potentially speeding up the time it takes to produce a hash without diminishing
# the security of the result. By default, the number of threads is set to the number
# of logical cores on your machine. If you set the number of threads to a number
# greater than the number of lanes, `Hasher` will automatically reduce the number
# of threads to the number of lanes

hasher.variant = Variant.Argon2id  # Default is Variant.Argon2id
# 👆 Argon2 has three variants: Argon2d, Argon2i, and Argon2id. Here is how these
# variants are explained in the RFC: "Argon2 has one primary variant: Argon2id,
# and two supplementary variants: Argon2d and Argon2i. Argon2d uses data-dependent
# memory access, which makes it suitable for ... applications with no threats from
# side-channel timing attacks. Argon2i uses data-independent memory access, which
# is preferred for password hashing and password-based key derivation. Argon2id
# works as Argon2i for the first half of the first iteration over the memory, and
# as Argon2d for the rest, thus providing both side-channel attack protection and
# brute-force cost savings due to time-memory tradeoffs." If you do not know which
# variant to use, use the default, which is Argon2id

hasher.version = Version._0x13  # Default is Version._0x13
# 👆 Argon2 has two versions: 0x10 and 0x13. The latest version is 0x13 (as of 5/18).
# Unless you have a very specific reason not to, you should use the latest
# version (0x13), which is also the default

hash = hasher.hash(
    password='P@ssw0rd',
    salt='somesalt',       # You can set your own salt, or use the default: RandomSalt(32)
)
assert(hash == '$argon2id$v=19$m=4096,t=192,p=2$c29tZXNhbHQ$8nD3gRm+NeOcIiIrlnzDAdnK4iD+K0mVqFXowGs13M4')

verifier = Verifier(secret_key=None)
verifier.additional_data = None  # As with Hasher, you can configure a Verifier's additional data
verifier.backend = Backend.C     # As with Hasher, you can configure a Verifier's backend
verifier.threads = 2             # As with Hasher, you can configure a Verifier's threads

is_valid = verifier.verify(
    hash=hash,
    password='P@ssw0rd'
)
assert(is_valid)

Miscellaneous

mypy

  • argonautica uses mypy type annotations everywhere in the code, which, in the author's humble opinion, is a very useful form of documentation; so if you're ever confused about what types to use for arguments, just pop open the code and take a look at the function signatures.

Argon2

Argon2 is a convenience class that holds both a Hasher and a Verifier. If you'd like to use just one class that knows how both to hash and to verify, instantiate an Argon2. It works essentially the same way as Hasher and Verifier do.

from argonautica import Argon2

argon2 = Argon2(secret_key='somesecret')

hash = argon2.hash(password='P@ssw0rd')
print(hash)

is_valid = argon2.verify(hash=hash, password='P@ssw0rd')
assert(is_valid)

RandomSalt

  • RandomSalt is a special kind of salt that will create new random salt bytes before each hash. A RandomSalt knows its length (in number of bytes). The default Hasher uses a RandomSalt with length of 32 bytes, but you can use your own RandomSalt of custom length. When you instantiate a RandomSalt, the constructor takes a length, e.g. my_random_salt = RandomSalt(16)
from argonautica import Hasher
from argonautica.data import RandomSalt

hasher = Hasher(
    salt=RandomSalt(16),
    # 👆 Here we're using a RandomSalt of length of 16 bytes
    # instead of the default, which is a RandomSalt of length 32 bytes
    secret_key="somesecret"
)
hash = hasher.hash(password='P@ssw0rd')
print(hash)

HashRaw

  • Hashing with argonautica produces a string-encoded hash, but sometimes you might want the "raw material" behind this hash, i.e. the raw hash bytes, the raw salt bytes, or raw parameters, which are the three component parts of a string-encoded hash. To obtain these raw parts...
from argonautica.utils import decode, HashRaw

hash = '$argon2id$v=19$m=4096,t=128,p=2$c29tZXNhbHQ$WwD2/wGGTuw7u4BW8sLM0Q'

# Create a `HashRaw` using the `decode` function
hash_raw = decode(hash)

# Pull out the raw parameters
iterations = hash_raw.iterations     # 128
lanes = hash_raw.lanes               # 2
memory_size = hash_raw.memory_size   # 4096
variant = hash_raw.variant           # Variant.Argon2id
version = hash_raw.version           # Version._0x13

# Pull out the raw bytes
raw_hash_bytes = hash_raw.raw_hash_bytes  # b'[\x00\xf6\xff\x01\x86N\xec;\xbb\x80V\xf2\xc2\xcc\xd1'
raw_salt_bytes = hash_raw.raw_salt_bytes  # b'somesalt'

# Turn a `HashRaw` back into a string-encoded hash using the `encode` method
hash2 = hash_raw.encode()
assert(hash == hash2)

License

argonautica is licensed under either of:

at your option.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

argonautica-0.1.4.tar.gz (1.9 MB view hashes)

Uploaded Source

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page