There are various methods for comparing two long strings in constant time.
For instance, you can compute a bitwise XOR of the two strings, then a bitwise OR of the words of the result.
Whether the final result is null or not shows whether the strings are equal or not, without providing any information about which are the differences, like in the standard scanning method.
This is the general method, all the operations required in the worst case must be executed always, without attempting to use clever tricks, which for certain particular inputs may skip some operations as unnecessary, thus providing information that those inputs have been encountered.
The kind of key comparison that has been given as an example of a mistake was already forbidden by Shannon's principle of confusion (published in a classified paper in 1945, declassified in 1949).
Many people claim to speak about Shannon's "diffusion and confusion", but they have not actually read Shannon's paper, so they guess wrong what Shannon has written. What Shannon has named "confusion" was that in any operation where a secret key or any other secret value is involved, any part of the result or any other observable property must depend equally of all the bits of the secret value (i.e. the bits of the secret value must be confused), so that the enemy is not able to extract any kind of information that applies to a part of the secret value, instead of to the whole secret value.
Computing a MAC of the two input values under a once-off random key is actually much stronger.
As a matter of that, this highlights that the goal is to lower the SNR for the attacker, and constant time computation is only one of the two non-exclusive ways to achieve that, the other being adding sufficient noise to their measurements.
The short-circuiting could be prevented e.g. by storing the result in a volatile variable before testing if it is null, because the compiler cannot assume that the tested value is the same that has been written previously. Nevertheless, it is better to just check the generated assembly code and disable optimizations for that function or use inline assembly, if necessary.
The binary Boolean operations have been executed in constant-time in all electronic computers, since those made with vacuum tubes until now.
It is impossible to make them execute in variable time (because they are independent for all bit pairs and they must be executed for all of them), unless you do this on purpose, by inserting unnecessary delays that are dependent on the operands.
For no other operations implemented in hardware it is as certain that they are done in constant time. Even for word additions, it would be possible to make an implementation where the time to propagate the carry until the most significant bit would be variable, depending on the pattern of "1" bits in the operands, but no mainstream CPU has used such adders during the last half of century. Such adders have been used only in some early computers with vacuum tubes, which used serial adders instead of the parallel adders used in all modern CPUs.
I think that is just an assumption and I would not take that risk for high security applications, unless for specific CPU models where the behavior is measured in practice (certainly not by just auditing the assembly, which is by itself already too high-level). After all, never in history we had such a level of sophistication.
Right now, all zero register values can lead to speed gains, so they could be obversable. But both ARM and Intel latest ISA have introduced flags that permit the future CPUs to perform operations with data-dependent timing. Boolean operations are officially marked as potentially affected by that flag.
Moreover, they are the only commonly encountered operations that are implemented in hardware and for which this is unconditionally true.
Therefore they are the first choice for the implementation of any constant-time algorithm.
Most modern CPUs have many other operations that are executed in constant time, like additions and subtractions, but for those, variable-time implementations are also possible.
As I have already said, for any bitwise binary boolean operation, the elementary operations having a pair of bits as operands are independent, so there exists no way of performing the complete operation without doing all the sub-operations, unlike for other simple operations, like additions, shifts or rotations, where it is possible to detect conditions that allow an earlier completion of the operation.
The hardware implementation can do the sub-operations sequentially or concurrently, in any order, but it always must execute all of them, regardless of the values of the input operands, so they will take the same time.
Only in an asynchronous CPU and only for certain kinds of logic gate implementations it is possible to have a completion time for a binary Boolean operation that varies with the values of the input bits, but for the most common asynchronous circuit synthesis methods, which use dual rail encoding for bits, i.e. each bit is encoded as a pair of complementary bits, the binary boolean operations remain constant-time, like in the normal synchronous CPUs.
If the CPU can pre-label a register as having no bits set (and they can or speculate on it), during scheduling, it could theoretically simply drop the XOR, transfer or rename the relevant register which may lead to a tiny but different timing that can be measured and exploited.
That is just one simple counter-example that shows how the assumptions you present are not necessarily valid on modern complex CPUs. Many more are examples are possible, which is of course not to say that they are implemented today. But they could be implemented in the future (without us knowing, and again, both ARM and Intel are explicit on that), therefore the security of a security-sensitive piece of code should not solely rely on that.
>The attacker can crack the first byte of the key by trying all 256 possibilities, and observing which one caused the comparison to take longer.
It means that at the end you get the hash and not the key, but it is still a vulnerability
Even if they discover hash(b), they can't produce b.
Of course it's way less impactful than discovering b, but it's nonetheless a vulnerability.
Consider what happens to the first byte of the hash if you add a new byte to the input, e.g. "1230"...
To be practical: my first guesses are:
0000...0000
1000...0000
2000...0000
...
f000...0000
I discover that c000...0000 takes slightly longer. Then, my next guesses are:
c000...0000
c100...0000
c200...0000
...
cf00...0000
Now I discovered that c700...0000 takes slightly longer...
And so on.
At the end I have the full hash.
Getting the plaintext key from that hash is a different problem.
UPDATE: damn, I understood what you mean now. Sorry. Yes, it will take a lot of computation to generate the good hashes.
This is not an easy problem, and in fact is what is used as proof-of-work for many cryptocurrencies.
So if what you’re hashing has low entropy/randomness (say a password) then you can totally brute force it.
In fact if there’s not a salt you can look it up in a rainbow table, saving yourself some compute. Googling a hash also works sometimes. If you want to brute force look at hashcat.
Something like SHA256 protects about 128 bits of entropy. If you go much lower than that, you’ll be brute forced. You can look at hashcat benchmarks online to estimate the time and money necessary to perform the attack. Anyone can crack an average simple password using a single round of SHA256 in no time.
There is no practical preimage attack against MD5, it would work fine in this case.
Getting the first character of the hash is trivial, but as you get further and further out you need to do more and more computational work locally to guess a string which will hash to the prefix you already know (given that your hash is secure against Preimage attack). And then at the end you end up with the complete hash, which itself is extremely hard to get anything useful out of. (all of this is given that you use a hash that is resistant to Preimage attack, and preferably salting as well).
You can now perform an offline attack against the password that produced the hash.
It’s like you hacked into the database and got the password hashes.
Then, you fall back to something like hashcat.
If with a bit of luck you just hashed your password with a single round of SHA and did not use something cool like Argon2id then you’re toast. If your password is simple you’re toast too.
On the other hand, if you hashed a totally random key, the safety will rely on whether the hashing algorithm itself is constant time or not. You just moved the needle.
How are you going to do that?
All popular hashing procedures (sha, argon, etc) to my knowledge produce high difference output for small difference inputs. This means that even with one round of sha128[0] your comparison timings turn very erratic. I won't go as far as saying that they turn random, because somebody will jump on it, but what they do achieve is render timing attacks at least impractical for the next few decades.
At least to my own knowledge.
[0] please don't use sha128!!!
> The attacker can crack the first byte of the key by trying all 256 possibilities, and observing which one caused the comparison to take longer.
OK as a thought experiment, the algorithm is passed plaintext data A (the input that the hacker is trying to guess) and hashed data B.
It hashes A before starting a comparison and then does a naive check, an we have code like-
function checkApiKey(inputKey, correctHashedKey) {
inputHashedKey=sha256(inputKey)
if (inputHashedKey !== correctHashedKey) {
throw new Error("wrong key");
}
}
If we're using a decently secure hash algorithm, for every bit that changes in the input, we will create (indistinguishable from) random changes throughout the output. An attacker on this system can't just continually tweak the first byte of input to figure out when a longer comparison takes place, because there's no guarantee that doing that will ever result in the first byte of the hash matching the first byte of the stored hash. And in theory that doesn't actually give away any information about the full password, because if you've matched byte one, adding the next byte to your guess changes the whole hash in unpredictable ways.Constant time still removes this sort of avenue entirely, but this would seem to confound the attack (or at least make it much harder problem to solve).
Happy to be educated otherwise if someone that's actually an expert would like to chime in?
This is also assuming the secret is know in plain text – for properly stored passwords you now have hash2(hash1(input+salt1)+salt2)==hash2(prehashed-secret+salt2) with salt2 changing on each attempt.
Of course this is a more computationally expensive than a more direct time-invariant string compare such as bitwise-xor-secret-with-guess-then-bitwise-or-all-bytes-in-result-and-compare-that-to-0, so messing around with hashes & salts is probably not what you would do. The XOR-the-OR method isn't perfect, it will still at least leak the length of the secret if it is unhashed (hashing just before the comparison won't save this because the hashing itself is not time invariant).
function checkApiKey(inputKey, correctKey) {
r = randomString()
inputHash = hash(r + inputKey)
correctHash = hash(r + correctKey)
if (inputHash !== correctHash) {
throw new Error("wrong key");
}
}