Public-key cryptography is great, but static shared symmetric keys are simpler and faster.
An API token can be a random byte sequence stored in a database. If two servers are operated by the same company, they can share a symmetric key.
Applications often use data with a signature to prevent tampering. JWT tokens are a common example. When signatures are generated and verified by the same organization, a HMAC construction (like HS256) is simpler and faster than public-key cryptography.
Since TLS is widely used, this may not seem risky. But if a server is compromised, if a secret key appears in a leaked SQL dump, or if it’s hardcoded in a public repository, attackers can forge valid tokens. Even without a leak, too many employees may have access to the secret.
Public-key cryptography or hash chains would provide stronger security, but static shared secrets can be improved with a simple tweak.
Improving static API tokens
The simplest implementation of static shared secrets works as follows:
- A random, static secret
xis generated and stored on both the client and server. - To authenticate, the client sends
xto the server. - The server verifies that the received value matches
x.
This protocol can be improved with a small change:
- A random, static secret
xis created. xis stored only on the client.- The server stores
z = HASH(x), notxitself. - To authenticate, the client sends
x. - The server computes
HASH(x)and verifies that it matches the stored valuez.
This may seem as weak as the original method, but the server no longer stores the actual secret. Assuming HASH is a secure hash function, leaking z does not allow an attacker to authenticate. z can be hardcoded into applications and remain publicly visible, while x stays only on the client.
x can still be leaked if the client is compromised, if the connection is intercepted, or if the server logs received values. But this is comparable to hashing passwords instead of storing them in plain text. It improves security with minimal effort. Since secrets are not subject to dictionary attacks, a single round of a fast hash function is sufficient.
Improving authentication tokens
Authentication tokens use a secret key to sign and verify arbitrary data. This is how HS256 JWT tokens work. A typical process using a static shared secret:
- A random, static secret
kis created and shared between the signer and verifier. - To ensure
datacannot be modified, the signer computest = HMAC(k, data). - The signer sends
dataandtto the verifier. - The verifier recomputes
HMAC(k, data)and checks that it matchest. Since generating a validtrequires knowingk, unauthorized modifications are prevented.
This process can be enhanced as follows:
- A random, static secret
kis created and shared between the signer and verifier. - Another secret
uis generated, stored only on the signer. The verifier storesz = HASH(u)instead. - To sign data, the signer computes
t = u || HMAC(k || u, data). - The signer sends
dataandtto the verifier. - The verifier extracts
uand the MAC fromt. It then verifies thatHASH(u)matcheszand thatHMAC(k || u, data)matches the received MAC.
This doesn’t improve security on the signer’s side, but it helps the verifier. Since the verifier no longer stores the full key, leaking z alone is insufficient for forging valid tokens. An attacker would also need u.
The same process can be applied to encryption using static, shared keys.
Simple and effective
Splitting keys has many security applications. The examples above are some of the simplest. Despite being easy to implement, this technique is underutilized.
This trick doesn’t turn basic authentication mechanisms into a silver bullet, but it improves security with minimal effort.