Password hashing¶
Password hashing and password based key derivation mechanisms in
actual use are all based on the idea of iterating a hash function
many times on a combination of the password and a random salt
,
which is stored along with the hash, and allows verifying a proposed
password while avoiding clear-text storage.
The latest developments in password hashing have been memory-hard
mechanisms, pioneered by the scrypt
mechanism [SD2012], which
is implemented by functions exposed in nacl.pwhash
.
Scrypt usage¶
Password storage and verification¶
The scryptsalsa208sha256_str()
internally
generates a random salt, and returns a scrypt hash already encoded
in ascii modular crypt format, which can be stored in a shadow-like file:
>>> import nacl.pwhash
>>> password = b'my password'
>>> for i in range(4):
... print(nacl.pwhash.scryptsalsa208sha256_str(password))
...
b'$7$C6..../....p9h...'
b'$7$C6..../....pVs...'
b'$7$C6..../....qW2...'
b'$7$C6..../....bxH...'
To verify a user-proposed password, the
scryptsalsa208sha256_verify()
function
extracts the used salt and scrypt memory and operation count parameters
from the modular format string and checks the compliance of the
proposed password with the stored hash:
>>> import nacl.pwhash
>>> hashed = (b'$7$C6..../....qv5tF9KG2WbuMeUOa0TCoqwLHQ8s0TjQdSagne'
... b'9NvU0$3d218uChMvdvN6EwSvKHMASkZIG51XPIsZQDcktKyN7'
... )
>>> correct = b'my password'
>>> wrong = b'My password'
>>> # the result will be True on password match
... # on mismatch
... res = nacl.pwhash.verify_scryptsalsa208sha256(hashed, correct)
>>> print(res)
True
>>>
>>> res2 = nacl.pwhash.verify_scryptsalsa208sha256(hashed, wrong)
Traceback (most recent call last):
...
nacl.exceptions.InvalidkeyError: Wrong password
>>>
Key derivation¶
Alice needs to send a secret message to Bob, using a shared
password to protect the content. She generates a random salt,
combines it with the password using
kdf_scryptsalsa208sha256()
and sends
the message along with the salt and key derivation parameters.
from nacl import pwhash, secret, utils
ops = pwhash.SCRYPT_OPSLIMIT_SENSITIVE
mem = pwhash.SCRYPT_MEMLIMIT_SENSITIVE
salt = utils.random(pwhash.SCRYPT_SALTBYTES)
password = b'password shared between Alice and Bob'
message = b"This is a message for Bob's eyes only"
Alices_key = pwhash.kdf_scryptsalsa208sha256(secret.SecretBox.KEY_SIZE,
password, salt,
opslimit=ops, memlimit=mem)
Alices_box = secret.SecretBox(Alices_key)
nonce = utils.random(secret.SecretBox.NONCE_SIZE)
encrypted = Alices_box.encrypt(message, nonce)
# now Alice must send to Bob both the encrypted message
# and the KDF parameters: salt, opslimit and memlimit;
# using the same parameters **and password**
# Bob is able to derive the correct key to decrypt the message
Bobs_key = pwhash.kdf_scryptsalsa208sha256(secret.SecretBox.KEY_SIZE,
password, salt,
opslimit=ops, memlimit=mem)
Bobs_box = secret.SecretBox(Bobs_key)
received = Bobs_box.decrypt(encrypted)
print(received)
if Eve manages to get the encrypted message, and tries to decrypt it with a incorrect password, even if she does know all of the key derivation parameters, she would derive a different key. Therefore the decryption would fail and an exception would be raised:
>>> from nacl import pwhash, secret, utils
>>>
>>> ops = pwhash.SCRYPT_OPSLIMIT_SENSITIVE
>>> mem = pwhash.SCRYPT_MEMLIMIT_SENSITIVE
>>>
>>> salt = utils.random(pwhash.SCRYPT_SALTBYTES)
>>>
>>> guessed_pw = b'I think Alice shared this password with Bob'
>>>
>>> Eves_key = pwhash.kdf_scryptsalsa208sha256(secret.SecretBox.KEY_SIZE,
... guessed_pw, salt,
... opslimit=ops, memlimit=mem)
>>> Eves_box = secret.SecretBox(Eves_key)
>>> intercepted = Eves_box.decrypt(encrypted)
Traceback (most recent call last):
...
nacl.exceptions.CryptoError: Decryption failed. Ciphertext failed ...
[SD2012] | A nice overview of password hashing history is available in Solar Designer’s presentation Password security: past, present, future |