0

As we can see from the following questions:

Java HmacSHA256 with key

Java vs. Golang for HOTP (rfc-4226)

, Java doesn't really play nicely when using a key in a TOTP / HOTP / HmacSHA256 use case. My analysis is that the following cause trouble:

  • String.getBytes will (of course) give negative byte values for characters with a character value > 127;
  • javax.crypto.Mac and javax.crypto.spec.SecretKeySpec both externally and internally use byte[] for accepting and transforming the key.

We have acquired a number of Feitian C-200 Single Button OTP devices, and they come with a hexadecimal string secret which consist of byte values > 127.

We have successfully created a PoC in Ruby for these tokens, which works flawlessly. Since we want to integrate these in Keycloak, we need to find a Java solution.

Since every implementation of TOTP / HOTP / HmacSHA256 we have seen makes use the javax.crypto library and byte[], we fear we have to rewrite all the used classes but using int in order to support this scenario.

Q: Is there another way? How can we use secrets in a HmacSHA256 calculation in Java of which the bytes have values > 127 without having to rewrite everything?


Update

I was looking in the wrong direction. My problem was that the key was represented a String (UTF-16 in Java), which contained Unicode characters that were exploded into two bytes by getBytes(), before being passed into the SecretKeySpec.

Forcing StandardCharsets.ISO_8859_1 on this conversion fixes the problem.

  • Java crypto treats the bytes as unsigned and works fine, and has worked fine for billions of users for decades; see my new answer to your first linked question which demonstrates this. – dave_thompson_085 Aug 20 '18 at 21:32

3 Answers3

2

Signed vs. unsigned is a presentation issue that's mainly relevant to humans only. The computer doesn't know or care whether 0xFF means -1 or 255 to you. So no, you don't need to use ints, using byte[] works just fine.

This doesn't mean that you can't break things, since some operations work based on default signed variable types. For example:

byte b = (byte)255;    // b is -1 or 255, depending on how you interpret it
int i = b;      // i is -1 or 2³² instead of 255
int u = b & 0xFF; // u is 255

It seems to throw many people off that Java has only signed primitives (boolean and char not withstanding). However Java is perfectly capable of performing cryptographic operations, so all these questions where something is "impossible" are just user errors. Which is not something you want when writing security sensitive code.

Kayaman
  • 72,141
  • 5
  • 83
  • 121
  • Excellently written. Note that addition and subtraction modulo 256 work exactly the same for signed or unsigned values. 127 + 1 = -128 and 127 + 1 = 128 are identical when you look at the bits: 0x7F + 0x01 = 0x80. That 0x80 can mean -128 or 128, so no problem at all. That's one of the nice things about the *two-complement* way of using negative values which each and every CPU uses nowadays. Just like bytes are always 8 bits nowadays :) – Maarten Bodewes Aug 20 '18 at 20:55
  • ITYM (IHYM!) Java has only >>signed<< primitives if you ignore `char` and `boolean` – dave_thompson_085 Aug 20 '18 at 21:30
  • @Kayaman Thanks for the explanation and pointers. I will revisit my usecase to see why I was led to this conclusion and follow up here. – Chris Brandhorst Aug 21 '18 at 08:11
  • @ChrisBrandhorst at least you don't need to write your own crypto framework :) – Kayaman Aug 21 '18 at 08:16
  • @Kayaman Indeed :) My problem lied in the source of the key, a String containing Unicode characters of which the conversion to byte array was incorrect. Thanks again. – Chris Brandhorst Aug 21 '18 at 14:00
0

Don't be afraid of Java :) I've tested dozens tokens from different vendors, and everything is fine with Java, you just need to pickup correct converter.

It's common issue to get bytes from String as getBytes() instead of using proper converter. The file you have from your vendor represent secret keys in hex format, so just google 'java hex string to byte array' and choose solution, that works for you.

Hex, Base32, Base64 is just a representation and you can easily convert from one to another.

Denis Shokotko
  • 225
  • 1
  • 6
0

I've ran into absolutely the same issue (some years later): we got Feitian devices, and had to set up their server side code.
None of the available implementations worked with them (neither php or java).

Solution: Feitian devices come with seeds in hexadecimal. First you have to decode the seed into raw binary (e.g. in PHP using the hex2bin()). That data is the correct input of the TOTP/HOTP functions.
The hex2bin() version of java is a bit tricky, and its solution is clearly written in the question of the OP.

(long story short: the result of hex2bin you have to interpret with StandardCharsets.ISO_8859_1, otherwise some chars will be interpreted as 2 bytes utf-16 char, which causes different passcode at the end)

String hex = "1234567890ABCDEF"; // original seed from Feitian

Sring secretKey = new String(hex2bin(hex), StandardCharsets.ISO_8859_1);
Key key = new SecretKeySpec(secretKey.getBytes(StandardCharsets.ISO_8859_1), "RAW");
// or without String representation:
Key key = new SecretKeySpec(hex2bin(hex), "RAW");
steve
  • 615
  • 6
  • 14