1
\$\begingroup\$

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?

\$\endgroup\$
5
  • \$\begingroup\$ So if input has a length of 10 and secret has a length of 5, what will secret[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\$
    – Marvin
    Nov 16, 2023 at 14:15
  • 1
    \$\begingroup\$ @Marvin In JavaScript it will return undefined \$\endgroup\$
    – Azerum
    Nov 16, 2023 at 14:32
  • \$\begingroup\$ Where are you running this Javascript (previously Typescript) code? \$\endgroup\$ Nov 16, 2023 at 14:35
  • \$\begingroup\$ @KIKOSoftware The code runs on Node.js. I've edited the question \$\endgroup\$
    – Azerum
    Nov 16, 2023 at 14:43
  • 2
    \$\begingroup\$ You've got a clear question, and the environment to test it. Did you do that? To prevent timing attacks you could simply add a random short delay in this function. \$\endgroup\$ Nov 16, 2023 at 14:54

1 Answer 1

0
\$\begingroup\$

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.

\$\endgroup\$
1
  • \$\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. Set bufferToCompare = input.length === secret.length ? input : badSecret. 4. Xor bufferToCompare and secret, sum, and check if result is 0 \$\endgroup\$
    – Azerum
    Nov 16, 2023 at 23:20

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.