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?
1 Answer 1
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
- Module to roll a new USER_KEY and transmit it to server
- Module to receive and store a client's new USER_KEY
- Client code to send an API request
- 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.
-
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– user2836962024年06月14日 06:56:01 +00:00Commented Jun 14, 2024 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\$Maarten Bodewes– Maarten Bodewes2024年06月28日 11:22:24 +00:00Commented Jun 28, 2024 at 11:22
https://
\$\endgroup\$