A simple tweak to make static shared keys suck less

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 x is generated and stored on both the client and server.
  • To authenticate, the client sends x to the server.
  • The server verifies that the received value matches x.

This protocol can be improved with a small change:

  • A random, static secret x is created.
  • x is stored only on the client.
  • The server stores z = HASH(x), not x itself.
  • To authenticate, the client sends x.
  • The server computes HASH(x) and verifies that it matches the stored value z.

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:

  1. A random, static secret k is created and shared between the signer and verifier.
  2. To ensure data cannot be modified, the signer computes t = HMAC(k, data).
  3. The signer sends data and t to the verifier.
  4. The verifier recomputes HMAC(k, data) and checks that it matches t. Since generating a valid t requires knowing k, unauthorized modifications are prevented.

This process can be enhanced as follows:

  1. A random, static secret k is created and shared between the signer and verifier.
  2. Another secret u is generated, stored only on the signer. The verifier stores z = HASH(u) instead.
  3. To sign data, the signer computes t = u || HMAC(k || u, data).
  4. The signer sends data and t to the verifier.
  5. The verifier extracts u and the MAC from t. It then verifies that HASH(u) matches z and that HMAC(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.