Here's a JavaScript function that check the equality of input
and secret
strings, trying to do that without leaking information about secret
:
function timeSafeEqual(input, secret) {
let equal = input.length === secret.length
for (let i = 0; i < input.length; ++i) {
const ofInput = input[i]
const ofSecret = secret[i]
if (ofInput !== ofSecret) {
equal = false
}
}
return equal
}
The function seems to always run in O(input.length)
time. Would you consider it to be timing-safe? This code runs on Node.js
Maybhe the time to compare undefined
string
values might be different than the time to compare string
and string
in V8?
1 Answer 1
Is it safe?
No.
bug
let equal = input.length === secret.length
In the happy path we assign True and life is good.
When we assign False, then de-referencing input[i]
and secret[i]
is trouble.
Minimally, the attempted de-reference will consume a perceptibly
different amount of delay.
If we get an undefined
back, then XORing it will be trouble.
Assume we assigned equal = True
above.
timing of a conditional
if (ofInput !== ofSecret) {
equal = false
}
This is just Bad.
We perform N comparisons, so that much is fine, there's no early exit.
But for M out of N mis-matches, we're performing an extra M assignments which an adversary armed with a stopwatch can observe. Total failure.
fix
Compute and store N xor's. In the case that the strings are equal, we store N zeros.
Sum the xor results and return that. Non-zero means the strings don't match.
Consider hashing both strings initially, then compare the hashes. One advantage is you're guaranteed to be comparing hashes of identical length. Another is that you have flexibility to pre-hash on a different host or at a previous time.
random jitter
Your adversary's ability to observe what's happening is impacted by random noisy delays caused by router queues and host scheduler queues.
Roll a random number and sleep that long before returning a boolean result. That way the adversary has less effective signalling bandwidth for his observations, so effective attacks will take much longer.
-
\$\begingroup\$ Indeed, all great points, thank you! I ended up using this algorithm: 1. Store secret in memory buffer. 2. Create
badSecret
buffer by flipping all bits of secret. 3. SetbufferToCompare = input.length === secret.length ? input : badSecret
. 4. XorbufferToCompare
andsecret
, sum, and check if result is 0 \$\endgroup\$Azerum– Azerum2023年11月16日 23:20:35 +00:00Commented Nov 16, 2023 at 23:20
input
has a length of10
andsecret
has a length of5
, what willsecret[5]
return? Honest question, I don't know TypeScript too well and other languages will throw an exception for out of bounds access, likely giving away the actual length of the secret. \$\endgroup\$undefined
\$\endgroup\$