When using the JavaX HMAC/SHA256 hashing libraries, if I right pad my secret key with non-zero bytes, the hash for the same message is different; as expected.
hmacSHA256digest( "secret".getBytes("UTF-8"), msg) = "244d9c89069406d40803722ec6a793e5e04c55234d9ca03039a7b505cb3f8f00"
hmacSHA256digest("secret\1".getBytes("UTF-8"), msg) = "4f94305c91ca9d8dec13ffcff7e455d6f0c49373e1bbc4035da2b500b11063fb"
However, if I right-pad the secret key with an arbitrary number of \0 bytes, the hash comes back as the same for different byte arrays like:
- "secret"
- "secret\0"
- "secret\0\0"
So, JavaX HMAC SHA256 is returning the same hash, even though the byte[] array returned from getBytes("UTF-8") for the secret just has a few additional zeros at the end (so it's not a UTF-8 issue):
hmacSHA256digest( "secret".getBytes("UTF-8"), msg)
= "244d9c89069406d40803722ec6a793e5e04c55234d9ca03039a7b505cb3f8f00"
hmacSHA256digest( "secret\0".getBytes("UTF-8"), msg)
= "244d9c89069406d40803722ec6a793e5e04c55234d9ca03039a7b505cb3f8f00"
hmacSHA256digest( "secret\0\0".getBytes("UTF-8"), msg)
= "244d9c89069406d40803722ec6a793e5e04c55234d9ca03039a7b505cb3f8f00"
Calls to other JavaX methods for MD5 and plain SHA256 do not return the same hash when extra \0 s are appended to the secret, so they pass our security test case for hash uniqueness across different secrets. Is the failure of this zero-padded-secrets case with MAC/SHA256 a possible attack vector?
This is the example code:
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
static void testRightZeroPaddedSecretsHaveDifferentHashes() {
try {
byte[] msg = "msg".getBytes("UTF-8");
// HMAC SHA256
byte[] b3 = hmacSHA256digest(msg, "secret".getBytes("UTF-8"));
byte[] b4 = hmacSHA256digest(msg, "secret\0".getBytes("UTF-8"));
// Plain SHA256
byte[] b5 = SHA256digest(msg, "secret".getBytes("UTF-8"));
byte[] b6 = SHA256digest(msg, "secret\0".getBytes("UTF-8"));
boolean same34 = Arrays.equals(b3, b4);
boolean same56 = Arrays.equals(b5, b6);
System.out.println(
"\n" + Arrays.toString(b3) +
"\n" + Arrays.toString(b4) +
"\nHMAC SHA256 - identical hash results? = " + same34 +
"\n" +
"\n" + Arrays.toString(b5) +
"\n" + Arrays.toString(b6) +
"\nPlain SHA256 - identical hash results? = " + same56
);
} catch (Throwable e) {
e.printStackTrace();
}
}
static byte[] hmacSHA256digest(byte[] msg, byte[] secret) {
try {
SecretKeySpec keySpec = new SecretKeySpec(secret, "HmacSHA256");
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(keySpec);
byte[] hmac = mac.doFinal(msg);
return hmac;
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
}
return null;
}
static byte[] SHA256digest(byte[] msg, byte[] secret) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
digest.update(msg);
byte[] hash = digest.digest(secret);
return hash;
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return null;
}
And sample output:
[-2, 79, -100, 65, -113, 104, 63, 3, 79, 106, -7, 13, 29, -43, -72, 106, -64, 53, 93, -39, 99, 50, -59, -100, -57, 69, -104, -48, 115, 97, 7, -10]
[-2, 79, -100, 65, -113, 104, 63, 3, 79, 106, -7, 13, 29, -43, -72, 106, -64, 53, 93, -39, 99, 50, -59, -100, -57, 69, -104, -48, 115, 97, 7, -10]
HMAC SHA256 - identical hash results? = true
[-88, 92, 89, -29, -65, -48, -127, 51, 125, -120, 78, -38, 25, 57, -91, 91, -50, 111, -33, 40, -3, 0, -95, 89, -50, -88, 39, 118, 101, -56, 91, 126]
[-40, 39, 49, -64, 58, 40, 124, 64, 110, -100, 50, 115, -32, 114, -107, 24, -73, -17, -37, 11, 67, -26, -48, -65, 109, -24, 119, 45, 74, -31, -81, 119]
Plain SHA256 - identical hash results? = false
Since JavaX HMAC SHA256 failed this zero-padded-secrets test case that passed for the plain SHA256/MD5 algorithms mentioned above, can anyone explain the difference in the behavior and if this can be exploited?
"secret\0\0".getBytes("UTF-8")
, I thought UTF-8 requires a minimal encoding. Maybe JavaX is trimming the trailing null bytes for the string in that case. Perhaps you should try withASCII
character set to get the raw bytes. Also see Convert String to/from byte array without encoding. – jww