How to make HMAC_SHA256 key from secret string to use it with JWT in jose4j?

29.6k Views Asked by At

I want to produce JWTs and sign them with HMAC_SHA256. For that task I must use jose4j. I have tried to generate key based on secret with:

SecretKeySpec key = new SecretKeySpec(("secret").getBytes("UTF-8"), AlgorithmIdentifiers.HMAC_SHA512);

but it generates 40bits key while 512bit one is required for signing using HMAC_SHA256.

  • The primary issue - how to sign tokens with HMAC_SHA512 using jose4j?
  • Issue created by my approach solving issue above - how to make 512bit long secret key based on secret string?
5

There are 5 best solutions below

1
On

A common approach is to hash the secret before using it as a signing key.

MessageDigest md = MessageDigest.getInstance("SHA-256");
String secret = "secret";
md.update(secret.getBytes("UTF-8"));
byte[] key = md.digest();

The alternative is to relax the requirement on the key length with something like:

JwtConsumer jwtConsumer = new JwtConsumerBuilder()
     .setVerificationKey(new HmacKey(secret.getBytes())) 
     .setRelaxVerificationKeyValidation() // allow shorter HMAC keys when used w/ HSxxx algs 
     .build();
1
On

Section 3.2 of JWA / RFC 7518 says that a key of the same size as the hash output or larger must be used with the JWS HMAC SHA-2 algorithms (i.e, 256 bits for "HS256", 384bits/"HS384", & 512 bits/"HS512"). It's generally a good idea to follow this advice from the IETF and NIST. Roughly speaking the security of an HMAC comes from the size of the hash output and the key length, whichever is smaller. So using the bytes of "secret" as the key gives you a key that's only 48 bits long and, in practice, provides considerably less security than even that because it's a dictionary word, regardless of the strength of the HMAC SHA-2 algorithm you chose.

By default jose4j enforces the minimum key sizes mandated by JWA/RFC 7518. However, as Hans points out, there are ways to tell jose4j to relax the key length requirement. This can be done with JwtConsumer by calling .setRelaxVerificationKeyValidation() on JwtConsumerBuilder and on JsonWebSignature directly with .setDoKeyValidation(false). Below is a quick example producing and consuming a JWT using HMAC SHA256 that shows both.

JwtClaims claims = new JwtClaims();
claims.setExpirationTimeMinutesInTheFuture(5);
claims.setSubject("foki");
claims.setIssuer("the issuer");
claims.setAudience("the audience");

String secret = "secret";
Key key = new HmacKey(secret.getBytes("UTF-8"));

JsonWebSignature jws = new JsonWebSignature();
jws.setPayload(claims.toJson());
jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.HMAC_SHA256);
jws.setKey(key);
jws.setDoKeyValidation(false); // relaxes the key length requirement

String jwt = jws.getCompactSerialization();
System.out.println(jwt);

JwtConsumer jwtConsumer = new JwtConsumerBuilder()
        .setRequireExpirationTime()
        .setAllowedClockSkewInSeconds(30)
        .setRequireSubject()
        .setExpectedIssuer("the issuer")
        .setExpectedAudience("the audience")
        .setVerificationKey(key)
        .setRelaxVerificationKeyValidation() // relaxes key length requirement 
        .build();

JwtClaims processedClaims = jwtConsumer.processToClaims(jwt);
System.out.println(processedClaims);
0
On

This will work:

import io.jsonwebtoken.io.Encoders;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.SignatureAlgorithm;
import javax.crypto.SecretKey;

...
SecretKey key = Keys.secretKeyFor(SignatureAlgorithm.HS512);
String secretString =  Encoders.BASE64.encode(key.getEncoded());
System.out.println("Kex: " + secretString);
...
3
On

For safe, 'secret' is too short and unsafe as a secret key. You can use the below code to generate a safe secret key as your personal secret key.

//Generating a safe HS256 Secret key
SecretKey key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
String secretString = Encoders.BASE64.encode(key.getEncoded());
logger.info("Secret key: " + secretString);
0
On

The answer can be found in RFC7515: https://www.rfc-editor.org/rfc/rfc7515#section-10.1

Let me quote it:

Keys are only as strong as the amount of entropy used to generate them. A minimum of 128 bits of entropy should be used for all keys, and depending upon the application context, more may be required. Implementations must randomly generate public/private key pairs, MAC keys, and padding values. The use of inadequate pseudorandom number generators (PRNGs) to generate cryptographic keys can result in little or no security. An attacker may find it much easier to reproduce the PRNG environment that produced the keys, searching the resulting small set of possibilities rather than brute-force searching the whole key space. The generation of quality random numbers is difficult. RFC 4086 offers important guidance in this area.

So, the best answer is that you don't use a password. Instead, you use a key generated by a CSPRNG.

If for some reason you insist on using a password or passphrase, good cryptography engineering practice require that you use a Key Derivation Function (KDF) to derive a key from the password. Argon2id is the KDF usually recommended these days. PBKD2 is an older option.

Using a password directly is likely to result in JWTs which can easily be brute forced by hashcat.