Skip to content

Security

James Brucker edited this page Jun 16, 2025 · 5 revisions

Password Security

Passwords should be stored as salted hashes. Good hashing algorithms, from strongest to weakest, are:

  1. Argon2 (especially Argon2id) — Strongest

    • Winner of the Password Hashing Competition; memory-hard, highly configurable, and resistant to GPU/ASIC attacks.
  2. scrypt

    • Also memory-hard and resistant to hardware attacks, but less flexible and slightly older than Argon2.
  3. bcrypt

    • Very well-tested and widely used, but less resistant to modern hardware attacks than Argon2 and scrypt.
  4. PBKDF2Weakest

    • Not memory-hard; more vulnerable to brute-force attacks with specialized hardware, but still secure with high iteration counts. But why choose it? A "high iteration count" today may not be good enough in a few years.

Applicability to an application using FastAPI + Postgres on backend, Vue.js on frontend.

  1. First choice should be Argon2, specifically, Argon2id.
  • Supported by the argon2-cffi library in Python.
  • In FastAPI use the argon2-cffi library for hashing and verifying passwords.
  • Store the hash (and salt, if not embedded) in the PostgreSQL database, as BYTEA for raw binary or TEXT for encoded strings.
  1. Second choice should be scrypt, which is provided by Python's hashlib module.
    Example:
    import os.urandom()
    import hashlib
    
    my_salt = os.urandom(16)         # 16 binary bytes
    hash = hashlib.scrypt(
             password=b'my_password',  # "bytes" object, at most 1024 bytes
             salt=my_salt,             # "bytes" object, recommended 16+ random bytes
             n=16384,                  # cpu/memory cost factor
             r=8,                      # blocksize
             p=1                       # parallelization factor
           )
    # hash is a "bytes" object.  To get a hex representation:
    hex_encoded = hash.hex()
    # Get in base64 form, which is more compact than hex (88 bytes vs 128 bytes):
    import base64
    b64_encoded = base64.standard_b64encode(hash)
    # use base64.standard_b64decode() to get back the original hash "bytes"

The values of n, r, and p are not necessarily secure. This 2017 post has a clear, short explanation of the purpose of each parameter.

Param Typical value Meaning
n 16384 Iteration count, affects time and memory. Must be power of 2.
r 8 Block size, linearly increases both memory and cpu use.
p 1 Parallelization. Number of core to use.

The memory required to compute the scrypt hash is:

Memory required = 128 * n * r * p  Bytes
# Example       = 128 * 16384 * 8 * 1 = 16 MB

On a laptop computer with 1.6 GHz Intel Core i5 and 8GB of memory running Ubuntu 20.04, the Python scrypt using the above parameters required 0.06 seconds for a 16-char password.

When I doubled r to r=16, Python 3.11 threw an out of memory error.

Clone this wiki locally