Nerd For Tech
Published in

Nerd For Tech

Checking API Key without shooting yourself in the foot (JavaScript, NodeJS)

Hooded person uses two laptops, looking dodgy
Photo by Azamat E on Unsplash

How API key-based authentication works

if (req.body.apiKey == expectedApiKey) {
// Authorize access
} else {
response.status(401).send('unauthorized');
}

What is a timing attack?

Why exactly is the naive approach vulnerable?

bool operator==(string op1, string op2) {
if(op1.length != op2.length) return false;
for(let i = 0; i < op1.length; ++i) {
if op1[i] != op2[i] return false;
}
return true;
}

How to fix the vulnerability?

bool timingSafeEqual(string op1, string op2) {
bool eq = true;
for(let i = 0; i < Math.min(op1.length, op2.length); ++i) {
if op1[i] != op2[i] eq = false;
}
if op1.length != op2.length eq = false;
return eq;
}

Lets look at our options

if (crypto.timingSafeEqual(Buffer.from(req.body.apiKey), Buffer.from(expectedApiKey)) {
// Authorize access
} else {
response.status(401).send('unauthorized');
}
if (req.body.apiKey.length === expectedApiKey.length && crypto.timingSafeEqual(Buffer.from(req.body.apiKey), Buffer.from(expectedApiKey)) {
// Authorize access
} else {
response.status(401).send('unauthorized');
}
if (crypto.timingSafeEqual(Buffer.from(req.body.apiKey.padEnd(expectedApiKey.length).slice(0, expectedApiKey.length)), Buffer.from(expectedApiKey)) {
// Authorize access
} else {
response.status(401).send('unauthorized');
}
  1. It looks unclean. It can raise a few eyebrows in code review, and perhaps warrant a “WTF”.
  2. I’m concerned about the implementation of String.slice . Is it a constant-time algorithm? Or does it depend on input size?
    I haven’t looked inside NodeJS to verify.

My proposed solution

const hash = crypto.createHash('sha512');
if (crypto.timingSafeEqual(
hash.copy().update(req.body.apiKey).digest(),
hash.copy().update(expectedApiKey).digest()
)) {
// Authorize access
} else {
response.status(401).send('unauthorized');
}
  • For each pair of invocations to the function, if the plain text is the same then the cipher text is the same.
    This way we can compare the ciphers, and know that the plaintext is the same iff the ciphers are the same.
  • The output of the function is always of the same size (in bits).
    This way both inputs of timingSafeEqual are of the same size.
  • The function does the same amount of work regardless of input.
    This means that it doesn’t leak timing information.
  • Given plaintext, it’s computationally cheap to compute it’s matching cipher text.
    Given cipher text, it’s computationally prohibitively expensive to compute it’s matching plaintext.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store