Overview
Hashes, HMAC and password hashing, from .NET’s System.Security.Cryptography.
(import (std crypto hash))
(bytes->hex (hash-string "hello")) ; SHA-256, as hex
(hash-file "/srv/big.iso" #:algorithm Sha512) ; read a buffer at a time
(hmac-string key "message") ; HMAC-SHA256
(def stored (password-hash "hunter2")) ; "$pbkdf2-sha256$i=600000$..."
(password-verify "hunter2" stored) ; #t
All functions take #:algorithm. The default is Sha256. A
string is hashed as its UTF-8 bytes.
MD5 and SHA-1 are broken. Use them only for a protocol or a checksum that needs them. Do not use them to make a signature or to find out if data was changed by an attacker.
A digest is not a password hash. A digest is fast, so an
attacker who has the digests can try many passwords each second. Use
password-hash for passwords. It is slow on purpose.
Passwords. password-hash uses PBKDF2-HMAC with a random
16-byte salt from (std random). The result is a PHC string:
$pbkdf2-sha256$i=600000$<salt>$<hash>
The salt and the hash are base64 without padding. The string holds the
algorithm and the iteration count, so password-verify needs only
the password and the string. When you increase the iteration count, old
hashes still verify. password-needs-rehash? tells you which ones
to replace after the next correct login.
The password is hashed as its UTF-8 bytes, and it is not normalized. Two strings that look the same, but use different Unicode code points, give different hashes.
Password hashing takes time. At 600000 iterations, one hash
takes a few hundred milliseconds of one core. In a bjoroutine,
password-hash and password-verify run on a thread of
their own, so the fiber pool continues to run. A login page must limit
the number of attempts, because each attempt costs this time.
The salt comes from current-random. A salt must be
unique, but it need not be secret. In a with-random-seed, two
runs make the same salts. This is correct for a test, but do not hash
real passwords there.
Compare secrets with constant-time-equal?. The
= on arrays stops at the first difference. An attacker who can
measure the time can then find a token one byte at a time.
See also: hash-bytes, hmac-bytes, make-hasher, password-hash, constant-time-equal?
Reference
Types
HashAlgorithm
union ↑ Types
Md5, Sha1, Sha256, Sha384, Sha512, Sha3-256, Sha3-384 or Sha3-512.
MD5 and SHA-1 are for protocols and checksums only. SHA-3 is not
available on all platforms: see hash-algorithm-supported?.
See also: hash-algorithm-supported?, digest-length
Hasher
type alias ↑ Types
A hash or an HMAC that you give data to in parts.
A hasher is mutable. Do not use one from two fibers at the same time.
It is .NET’s IncrementalHash.
See also: make-hasher, make-hmac-hasher, hasher-add!, hasher-finish!
Functions
hash-algorithm-supported?
function ↑ Functions
(: hash-algorithm-supported? (-> HashAlgorithm bool))
(hash-algorithm-supported? a)
True when this platform can compute the algorithm.
a- The algorithm.
- returns
- False only for SHA-3 on a platform without it: OpenSSL older than 1.1.1, or Windows older than 11.
digest-length
function ↑ Functions
(: digest-length (-> HashAlgorithm int))
(digest-length a)
The length of the algorithm’s digest, in bytes.
a- The algorithm.
- returns
- 16 for MD5, 20 for SHA-1, and 32, 48 or 64 for the others.
hash-bytes
function ↑ Functions
(: hash-bytes (-> (Array byte) (#:algorithm HashAlgorithm) (Array byte)))
(hash-bytes data #:algorithm Sha256)
The digest of a byte array.
data- The bytes to hash.
#:algorithm- The algorithm. The default is Sha256.
- returns
- A new array of
(digest-length algorithm)bytes.
See also: hash-string, bytes->hex
hash-string
function ↑ Functions
(: hash-string (-> string (#:algorithm HashAlgorithm) (Array byte)))
(hash-string text #:algorithm Sha256)
The digest of a string’s UTF-8 bytes.
text- The string to hash.
#:algorithm- The algorithm. The default is Sha256.
- returns
- A new array of
(digest-length algorithm)bytes.
(bytes->hex (hash-string "abc"))
;; "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
See also: hash-bytes, bytes->hex
hash-file
function ↑ Functions
(: hash-file (-> string (#:algorithm HashAlgorithm) (Array byte)))
(hash-file path #:algorithm Sha256)
The digest of a file, read a buffer at a time.
path- The path of the file.
#:algorithm- The algorithm. The default is Sha256.
- returns
- A new array of
(digest-length algorithm)bytes.
In a bjoroutine, the read suspends the fiber. Elsewhere, it blocks the
thread. A file that is not there raises
System.IO.FileNotFoundException.
hmac-bytes
function ↑ Functions
(: hmac-bytes (-> (Array byte) (Array byte) (#:algorithm HashAlgorithm) (Array byte)))
(hmac-bytes key data #:algorithm Sha256)
The HMAC of a byte array.
key- The secret key. Use at least as many bytes as the digest has.
data- The bytes to authenticate.
#:algorithm- The algorithm. The default is Sha256.
- returns
- The tag: a new array of
(digest-length algorithm)bytes.
To check a tag that you received, compute the tag again and compare
the two with constant-time-equal?.
See also: hmac-string, constant-time-equal?
hmac-string
function ↑ Functions
(: hmac-string (-> (Array byte) string (#:algorithm HashAlgorithm) (Array byte)))
(hmac-string key text #:algorithm Sha256)
The HMAC of a string’s UTF-8 bytes.
key- The secret key.
text- The string to authenticate.
#:algorithm- The algorithm. The default is Sha256.
- returns
- The tag: a new array of
(digest-length algorithm)bytes.
See also: hmac-bytes, constant-time-equal?
make-hasher
function ↑ Functions
(: make-hasher (-> (#:algorithm HashAlgorithm) Hasher))
(make-hasher #:algorithm Sha256)
A new, empty hasher.
#:algorithm- The algorithm. The default is Sha256.
- returns
- A hasher that has no data yet.
(def h (make-hasher))
(hasher-add-string! h "hello, ")
(hasher-add-string! h "world")
(hasher-finish! h) ; the same as (hash-string "hello, world")
See also: hasher-add!, hasher-finish!
make-hmac-hasher
function ↑ Functions
(: make-hmac-hasher (-> (Array byte) (#:algorithm HashAlgorithm) Hasher))
(make-hmac-hasher key #:algorithm Sha256)
A new, empty HMAC hasher with a key.
key- The secret key.
#:algorithm- The algorithm. The default is Sha256.
- returns
- A hasher that has no data yet. It keeps the key after each
hasher-finish!.
See also: hasher-add!, hasher-finish!, hmac-bytes
hasher-add!
function ↑ Functions
(: hasher-add! (-> Hasher (Array byte) void))
(hasher-add! h data)
Gives bytes to a hasher.
h- The hasher.
data- The bytes.
hasher-add-string!
function ↑ Functions
(: hasher-add-string! (-> Hasher string void))
(hasher-add-string! h text)
Gives a string’s UTF-8 bytes to a hasher.
h- The hasher.
text- The string.
hasher-finish!
function ↑ Functions
(: hasher-finish! (-> Hasher (Array byte)))
(hasher-finish! h)
The digest of all the data given since the hasher was made or last finished.
h- The hasher.
- returns
- A new array of digest bytes.
The hasher is then empty, and you can use it again. An HMAC hasher keeps its key.
constant-time-equal?
function ↑ Functions
(: constant-time-equal? (-> (Array byte) (Array byte) bool))
(constant-time-equal? a b)
Compares two byte arrays in a time that does not depend on their contents.
a- One array.
b- The other.
- returns
- True when the arrays have the same length and the same bytes.
Use this to compare a secret: an HMAC tag, a token, a digest of a token. The time depends on the lengths, so an attacker can find the length. That is not a problem when the length is fixed, as it is for a digest.
bytes->hex
function ↑ Functions
(: bytes->hex (-> (Array byte) string))
(bytes->hex bytes)
The bytes as lower-case hex, two digits each.
bytes- The bytes.
- returns
- A string of two lower-case hex digits for each byte.
See also: hex->bytes
hex->bytes
function ↑ Functions
(: hex->bytes (-> string (Option (Array byte))))
(hex->bytes text)
The bytes that a hex string gives.
text- Hex digits, upper or lower case, two for each byte.
- returns
Nonewhen the length is odd or a character is not a hex digit.
See also: bytes->hex
password-hash
function ↑ Functions
(: password-hash (-> string (#:algorithm HashAlgorithm) (#:iterations int) string))
(password-hash password #:algorithm Sha256 #:iterations default-password-iterations)
A slow, salted hash of a password, as a PHC string to store.
password- The password.
#:algorithm- Sha256, Sha384 or Sha512. The default is Sha256. Other algorithms raise
ArgumentException. #:iterations- The PBKDF2 iteration count. The default is
default-password-iterations. - returns
- A string such as
$pbkdf2-sha256$i=600000$<salt>$<hash>.
Each call uses a new salt, so two hashes of the same password are different. In a bjoroutine, the hash is computed on a thread of its own.
See also: password-verify, password-needs-rehash?
password-verify
function ↑ Functions
(: password-verify (-> string string bool))
(password-verify password stored)
True when the password gives the stored hash.
password- The password to check.
stored- A string from
password-hash. - returns
- True or false. The hashes are compared in constant time.
A stored string that is not a PHC string from
password-hash raises ArgumentException. That is an error
in the stored data, not a wrong password.
In a bjoroutine, the hash is computed on a thread of its own.
See also: password-hash, password-needs-rehash?
password-needs-rehash?
function ↑ Functions
(: password-needs-rehash? (-> string (#:algorithm HashAlgorithm) (#:iterations int) bool))
(password-needs-rehash? stored #:algorithm Sha256 #:iterations default-password-iterations)
True when a stored hash uses another algorithm or fewer iterations.
stored- A string from
password-hash. #:algorithm- The algorithm that hashes must use now. The default is Sha256.
#:iterations- The iteration count that hashes must have now. The default is
default-password-iterations. - returns
- True when you must replace
storedwith a new hash. Astoredstring that is not a PHC string raisesArgumentException.
(when (password-verify password stored)
(when (password-needs-rehash? stored)
(save-hash! user (password-hash password)))
(log-in user))
Use it after a correct password-verify. Only then do you have the
password to make a new hash.
Values
default-password-iterations
value ↑ Values
(: default-password-iterations int)
600000: the OWASP recommendation for PBKDF2-HMAC-SHA256 in 2023.