# Appropriate key for AES 256 GCM

I am updating a system that has Rails' OpenSSL on one side and Python's cryptography.io on the other.

The Rails side has to generate keys for the AES-256-GCM and here are two methods that appear to work:

SecureRandom.hex
# => "3d499a7b8f57773281ca2d47698a0062"

OpenSSL::Cipher::Cipher.new("aes-256-gcm").random_key
# => "\x1A\xAC\x8F\xC4mX\xAE\xDE\x04\xE1\x83\x8F\xFF^\x8C3KJ\xBE\vt\xE4\xBC\x14B\xDEro\xB8\xF5Yy"


Due to the transmission, it is currently easier to use SecureRandom.hex, but I believe that its reduced charset would make it less secure for any given length. Is this a valid concern (meaning I should spend more time making the OpenSSL version work), or is it reasonable to use SecureRandom.hex to generate keys?

• This question seems reasonable to me. Why the -1? – sscirrus Apr 24 '18 at 19:15
• I didn't down-vote it, but I could imagine that this question looks a lot like a programming question ("should I use X or Y in ruby"). However as the question "is it secure to use a base64 encoded 24-byte random string directly for AES-256-GCM" it is certainly very intriguing (which would be my current reading of this question). – SEJPM Apr 24 '18 at 19:19

## 2 Answers

The different charsets are simply the result of different encodings. A secure random number generator will generate a sequence of bits indistinguishable from pure random. These bits can then be encoded in several ways. In your case the SecureRandom.hex is returning base64 encoded bits, while OpenSSL::Cipher::Cipher.new("aes-256-gcm").random_key is returning ASCII encoded bits (where bit sequences that have no ASCII representation are prefixed with \x and hex encoded).

These two methods do not, in fact, return the same number of random bits:

In [7]: from binascii import a2b_base64

In [8]: openssl = b'\x1A\xAC\x8F\xC4mX\xAE\xDE\x04\xE1\x83\x8F\xFF^\x8C3KJ\xBE\vt\xE4\xBC\x14B\xDEro\xB8\xF5Yy'

In [9]: len(openssl)
Out[9]: 32

In [10]: secrand = a2b_base64('Z6sBQRGKll7FEKz3ebfUiGupwhPS3QVm')

In [11]: len(secrand)
Out[11]: 24


As you can see, SecureRandom.hex generates 24 bytes = 192 bits, and OpenSSL::Cipher::Cipher.new("aes-256-gcm").random_key generates 32 bytes = 256 bits.

Since AES-256-GCM requires a 256 bit key I recommend using OpenSSL::Cipher::Cipher.new("aes-256-gcm").random_key.

EDIT

Since the value SecureRandom.hex returns in the question was updated to be a hexidecimal encoding the amount of random bits it generates is no longer 192. It actually generates less random bits now, since a base64 encoded string can encode more bits than an equally long hex encoded string.

In [1]: from binascii import unhexlify

In [2]: k = unhexlify('3d499a7b8f57773281ca2d47698a0062')

In [3]: len(k)
Out[3]: 16


So now we have 16 bytes = 128 bits.

In general, assuming a secure random number generator, the number of bytes of entropy that an encoded string of length $n$ has is:

• ASCII - $n$
• base64 - $\frac{3n}{4}$
• hex - $\frac{n}{2}$
• binary - $\frac{n}{8}$

You can multiply any of those terms by 8 to get the equivalent values in bits rather than bytes.

• This isn't quite right. In Rails, running SecureRandom.hex.length returns 32. My concern is more that the charset of SecureRandom.hex is 0-9 and a-f, whereas OpenSSL's version is any byte. – sscirrus Apr 24 '18 at 19:00
• The base64 encoded value's length is indeed 32 bytes, but the length of the bit sequence it encodes is only 24 bytes. – puzzlepalace Apr 24 '18 at 19:29

There is a difference between the value of the key and the representation of the key (for human consumption).

The key itself should always consist of bits / bytes indistinguishable from random. If you want to view the symmetric key you can choose between several methods of encoding: binary digits '0' or '1' for each bit, hexadecimals or base 64. It is important to understand that the key itself however consists of the original bytes.

Generally keys are not transported unencrypted. If any encoding is performed on the key for the reason of transport or storage then the key must be decoded before use, i.e. the encoding must be reversed.

So your keys consist of 24 bytes after base 64 decoding when SecureRandom.hex is used and 32 bytes after decoding the characters and hex escaped characters when the OpenSSL method is used. You should not use the encoding directly as a key.

[EDIT] the updated code shows 16 bytes encoded or represented by 32 hexadecimal characters for the .hex function. I'll leave it up to you to choose between OpenSSL and the .hex; I cannot do a full review of either option. For 32 byte keys you should of course request 32 bytes, certainly not use the 32 characters, character-encoded to bytes.

• Again, the comment got too long, upvoted puzzelpalace answer – Maarten - reinstate Monica Apr 24 '18 at 19:37
• That's useful, thanks. The keys are not being transmitted unencrypted - they are contained within an encrypted message using the previous key. – sscirrus Apr 24 '18 at 19:46
• Encryption can handle bytes perfectly well. So you can directly encrypt (or wrap) the key and - if required - encode / decode the encrypted message. The only reason to encode the key would be if it is to be part of XML or JSON encoded message. But note that strings are notoriously hard to remove from memory, especially for managed languages such as Ruby. – Maarten - reinstate Monica Apr 24 '18 at 19:50
• The issue I have isn't with the transport - it's more to do with the storage formats and usage on the Python side. I wasn't clear on how best to encode/decode OpenSSL's format, but with base64, this should be doable now. – sscirrus Apr 24 '18 at 19:52
• For keys I prefer hex because it is easier to see the size of the keys and the additional efficiency of base 64 is hardly required. But the choice isn't that important. However, if at all possible, I prefer no encoding, just binary or ideally just a reference to a key stored in a hardware device. – Maarten - reinstate Monica Apr 24 '18 at 19:55