3
\$\begingroup\$

I'm trying to create an API that will receive requests from users without having to do a login request. They will just have a user id and a key. My intention is that they will use the key to create a signature. And the server will recalculate the signature to check message integrity and that the correct key was used.

The code that the client would use to calculate the signature for the request would be something like this:

import crypto from "crypto";

const USER_ID = 35345;
// Key that will be used to sign the requests
const USER_KEY = crypto.randomBytes(32).toString("base64"); // This should be securely stored by the user

const requestParams = {
  id: 158787,
  amount: 24
};

// Using the timestamp will allow the server to decline old requests
const timestamp = Date.now();
const paramsInBase64 = Buffer.from(JSON.stringify(requestParams), "utf8").toString("base64");

// The key is scrypted here because server will not store the plain key, only the result of the scrypt
const scryptedKey = crypto.scryptSync(USER_KEY, String(USER_ID), 64).toString("base64");

// Combining the key with the timestamp to prevent it being always the same key for the hmac
const combinedKey = scryptedKey + timestamp;
const hashedKey = crypto.createHash("sha512").update(combinedKey).digest("base64");

const signature = crypto
  .createHmac("sha512", hashedKey)
  .update(USER_ID + paramsInBase64 + timestamp)
  .digest("base64");

const request = {
  params: paramsInBase64,
  signature: signature,
  user: USER_ID,
  timestamp: timestamp
};
console.log("request :", request);

The server side would receive the request and obtain the scrypted key from the database using the user id and repeat the same process to check if the signature is correct.

Is this secure enough for an API, or am I overlooking something?

\$\endgroup\$
4
  • 1
    \$\begingroup\$ Only if you are communicating via secure protocol. https:// \$\endgroup\$
    – Blindman67
    Commented Jun 13 at 12:14
  • \$\begingroup\$ Yes, I am using https. Thanks! \$\endgroup\$
    – user283696
    Commented Jun 14 at 6:51
  • \$\begingroup\$ Please don't edit the code in your question when it has been answered. I have rolled back the edit, so the answers make sense again. \$\endgroup\$ Commented Jun 14 at 7:04
  • \$\begingroup\$ The main problem with this post is that clearly tries to reinvent the wheel, and it fails to really explain the way key management is going to be implemented. It seems to be a combination of TOTP and message integrity / authentication. I was going to answer but since the author seems to have deregistered it doesn't make too much sense. \$\endgroup\$ Commented Jun 28 at 11:20

1 Answer 1

2
\$\begingroup\$

HMAC vs signature

secured only by the request signature

I don't understand the title, given that the OP code uses an HMAC instead of calling some PK .sign() routine.

HMAC offers integrity and authentication; we're convinced that both parties share the same secret key, even if one party is Mallet the attacker. When you mention "signature" you're suggesting that you also want non-repudiation.

Client Alice had to disclose her 256-bit secret to server Bob, and after that she has no idea how good or how leaky Bob's security setup is. So if Mallet p0wns Bob, Mallet can construct API messages saying Alice bought some expensive things which she will have trouble refuting. In contrast with e.g. an RSA signature, Alice hangs on to her private key. Mallet would have to compromise Alice to produce those forgeries.

sharing a secret

The USER_KEY assignment looks good. It was apparently put here for expository purposes, as it belongs in a different module -- we roll a new key just once, or perhaps once monthly.

And then the hard part is key management: securely transmitting it to server, securely storing it on both client and server, and securely expiring old keys. Getting the crypto math correct is easy, compared to the hard work of actually deploying an end-to-end real-world secure system. Alas, it turns out that "people are part of the system", and they are less predictable than computers.

wrong salt

This doesn't conform to how the documentation says you should use the interface.

const scryptedKey = crypto.scryptSync(USER_KEY, String(USER_ID), 64).toString("base64");

In particular, String(35345) is a low-entropy argument, which goes against the calling requirements:

The salt should be as unique as possible. It is recommended that a salt is random and at least 16 bytes long.

The comment offers insight on the author's state of mind:

// The key is scrypted here because server will not store the plain key, only the result of the scrypt

Thank you for writing that. But I disagree. Also, there is some review context missing, we should have seen

  1. Module to roll a new USER_KEY and transmit it to server
  2. Module to receive and store a client's new USER_KEY
  3. Client code to send an API request
  4. Server code to validate an API request

Notice that (2.) disappears entirely once we switch to PK crypto, e.g. RSA. Also, in many systems we find it convenient to have server generate 256 random bits which it discloses to client.

The comment would make perfect sense if the end user typed in a low-entropy easily remembered password, especially one that the user might use with multiple webservers. The idea is to make it expensive for Mallet to turn a breach of Bob into credentials usable on unrelated webservers. Entropy stretching a manually entered passphrase with scrypt or argon2id makes perfect sense when considering that threat environment.

But here? No. By hypothesis USER_KEY is already 256 perfectly random bits, it is plenty secure (other than we regrettably need Alice + Bob to know it.) We assume Alice will roll other keys when interacting with other servers. The scrypt operation doesn't buy us any extra security, and sadly it slows down our webserver for no gain.

tl;dr: Discard the scrypt call, and stick with just the HMAC.

standard terminology

Consider calling USER_KEY an API_KEY.

\$\endgroup\$
2
  • 1
    \$\begingroup\$ Thanks for the insightful response and sorry for missnoming some things. On the scrypt part i was thinking about the API_KEY as if it was a password, so I thougth it would be better to not store them in plain text on the database. But with your explanation I understand it is not necessary. \$\endgroup\$
    – user283696
    Commented Jun 14 at 6:56
  • \$\begingroup\$ Using a KDF to protect a password / key does make some sense, but in this case I think it runs against the use case. \$\endgroup\$ Commented Jun 28 at 11:22

Your Answer

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