Static secret as byte[], Key or String?

Paulo picture Paulo · Oct 26, 2016 · Viewed 16.1k times · Source

I have started to work with JJWT to handle JWT on my server application.

My JWT secret will be stored at resources folder and I will load the secret with Properties class.

The JJWT provides three methods to sign the JWT, one uses byte[], other uses String and the other uses Key:

JwtBuilder signWith(SignatureAlgorithm var1, byte[] var2);

JwtBuilder signWith(SignatureAlgorithm var1, String var2);

JwtBuilder signWith(SignatureAlgorithm var1, Key var2);

The question: Regarding security, charset and other things, there are any recommendations of which one I should use?

For while, I stand with String, since Properties return a String.

Answer

Les Hazlewood picture Les Hazlewood · Oct 27, 2016

With JJWT >= 0.10.0, signWith(SignatureAlgorithm var1, String var2) has been deprecated because of the confusion between raw strings and Base64-encoded strings:

/**
 * Signs the constructed JWT using the specified algorithm with the specified key, producing a JWS.
 *
 * <p>This is a convenience method: the string argument is first BASE64-decoded to a byte array and this resulting
 * byte array is used to invoke {@link #signWith(SignatureAlgorithm, byte[])}.</p>
 *
 * <h4>Deprecation Notice: Deprecated as of 0.10.0, will be removed in the 1.0 release.</h4>
 *
 * <p>This method has been deprecated because the {@code key} argument for this method can be confusing: keys for
 * cryptographic operations are always binary (byte arrays), and many people were confused as to how bytes were
 * obtained from the String argument.</p>
 *
 * <p>This method always expected a String argument that was effectively the same as the result of the following
 * (pseudocode):</p>
 *
 * <p>{@code String base64EncodedSecretKey = base64Encode(secretKeyBytes);}</p>
 *
 * <p>However, a non-trivial number of JJWT users were confused by the method signature and attempted to
 * use raw password strings as the key argument - for example {@code signWith(HS256, myPassword)} - which is
 * almost always incorrect for cryptographic hashes and can produce erroneous or insecure results.</p>
 *
 * <p>See this
 * <a href="https://stackoverflow.com/questions/40252903/static-secret-as-byte-key-or-string/40274325#40274325">
 * StackOverflow answer</a> explaining why raw (non-base64-encoded) strings are almost always incorrect for
 * signature operations.</p>
 *
 * <p>To perform the correct logic with base64EncodedSecretKey strings with JJWT >= 0.10.0, you may do this:
 * <pre><code>
 * byte[] keyBytes = {@link Decoders Decoders}.{@link Decoders#BASE64 BASE64}.{@link Decoder#decode(Object) decode(base64EncodedSecretKey)};
 * Key key = {@link Keys Keys}.{@link Keys#hmacShaKeyFor(byte[]) hmacShaKeyFor(keyBytes)};
 * jwtBuilder.signWith(key); //or {@link #signWith(Key, SignatureAlgorithm)}
 * </code></pre>
 * </p>
 *
 * <p>This method will be removed in the 1.0 release.</p>
 *
 * @param alg                    the JWS algorithm to use to digitally sign the JWT, thereby producing a JWS.
 * @param base64EncodedSecretKey the BASE64-encoded algorithm-specific signing key to use to digitally sign the
 *                               JWT.
 * @return the builder for method chaining.
 * @throws InvalidKeyException if the Key is insufficient or explicitly disallowed by the JWT specification as
 *                             described by {@link SignatureAlgorithm#forSigningKey(Key)}.
 * @deprecated as of 0.10.0: use {@link #signWith(Key)} or {@link #signWith(Key, SignatureAlgorithm)} instead.  This
 * method will be removed in the 1.0 release.
 */
JwtBuilder signWith(SignatureAlgorithm alg, String base64EncodedSecretKey);

This method expects the string argument to be a Base64-encoded secret key byte array. It does not assume a general string, like a user password for example, as the signing key. JJWT assumes Base64 encoding because if you're specifying a string password that is not Base64-encoded, you're probably using a poorly formed or weak key.

The JWT JWA specification REQUIRES that HMAC signing keys have lengths equal to or greater than the signature byte array length.

That means that:

| If you're signing with: | your key (byte array) length MUST be: |
| ----------------------- | ------------------------------------- |
| HMAC SHA 256            | >= 256 bits (32 bytes)                |
| HMAC SHA 384            | >= 384 bits (48 bytes)                |
| HMAC SHA 512            | >= 512 bits (64 bytes)                |

Many online JWT sites and tools just just get this plain wrong - they allow you to think that you could type in or use any old string and you're good. Some go as far as even pre-populating the key with the word secret (clearly a bad idea and not even spec-compliant because it's too short!).

To help simplify things for you, JJWT provides a utility to help you generate sufficient secure-random keys suitable for spec-compliant signing via the io.jsonwebtoken.security.Keys class's secretKeyFor method. For example:

//creates a spec-compliant secure-random key:
SecretKey key = Keys.secretKeyFor(SignatureAlgorithm.HS256); //or HS384 or HS512

If you wanted to store the generated key as a String, you could presumably Base64 encode it:

String base64Key = Encoders.BASE64.encode(key.getEncoded());

But note: the resulting base64Key string is not considered safe to show to anyone. Base64 encoding is not encryption - the value still needs to be kept secret. How you do this is up to you (encrypt it, etc).

Now, when it is time to create a JWS, you could pass in that base64Key value, and JJWT knows to base64 decode it first to get the real bytes, which are then used to compute the signature:

Jwts.builder()
    //...
    .signWith(SignatureAlgorithm.HS512, base64Key)
    .compact();

And while you could do this, it is not recommended per the above deprecation notice in the JavaDoc due to the ambiguity between raw strings and base64-encoded strings.

As a result, it is recommended to use either the JWT builder's signWith(Key) or signWith(Key, SignatureAlgorithm) methods which guarantee a type-safe Key argument. For example:

  Jwts.builder()
    //...
    .signWith(key) // or signWith(key, preferredSignatureAlgorithm)
    .compact();

signWith(Key) is recommended to let JJWT figure out the strongest algorithm possible based on the strength of your supplied key. signWith(Key,SignatureAlgorithm) allows you to specify a desired algorithm if you don't want the strongest possible one.

Both methods will reject any Key that doesn't meet the minimum RFC requirements.