Comparing hashes of strings does not fully defeat timing attacks.
For example, if we try to find a password by timing attack of
if (strcmp(sha256str(input),sha256str("honey7dew"))) ...
sha256str outputs the SHA-256 hash of its input as a 64-char C string in hexadecimal (4 bits per char), and we initially do not know
"honey7dew" but suspect that string might be a password with low entropy, we can do the following (assuming we can guess the number of characters matched by
- let k = "" (the hash prefix known so far)
- for each password p in a list of common passwords, by decreasing popularity
- let h =
- if h starts with k
- submit p as input and find the number n of matching characters
- if n is 64, p is the password, stop
- set k to the first n characters of h
and it will find the password with only a few hundreds of online submissions (this can be improved significantly by avoiding submission of p that we know are not right, because we previously submitted a p0 with h0 matching h over at least 1 more character than k is).
Update per comment: the attack works just the same if the hash is salted, and the salt known to the attacker (but if there are restrictions to the number of online queries for a given salt, these restrictions might make the attack impractical).
If the hash is entropy-stretching (e.g. Argon2), the attack essentially allows to find the
"honey7dew" low-entropy key/password with the same computational cost as if as if its hash was known, replacing that knowledge by a moderate number of online submissions.
"honey7dew" is replaced by something with high entropy (hopeless to guess), there demonstrably is no attack enabled by timing of the comparison: if there was one, it would break the first preimage resistance of the hash.
If the salt is unknown to the attacker (that's often called pepper) and large enough, it is essentially an extension of the the key/password, and again there demonstrably is no attack enabled by timing of the comparison.