Page MenuHome GnuPG

Gpgmepp getrandom zbase32 code has zero byte at the end
Closed, ResolvedPublic

Description

It is highly surprising that asking the gpgmepp code for 31 bytes zbase32 code that you get in return 30 bytes zbase32 and a nullbyte.
It is even more surprising that if you ask the gpgmepp code for 40 bytes of zbase32 code that you get in return 30 bytes zbase32 code and 10 nullbytes.

This is 1) a bug in gpgmepp that it preserves the c-string nullbyte and 2) a bug that numbers other than 30 is anything but an error.

Event Timeline

I can't speak for gpgmpp but for gpgme. And the gpgme manual says:

The function gpgme_op_random_bytes returns random bytes. buffer must be provided by the caller with a size of BUFSIZE and will on return be filled with random bytes retrieved from gpg. However, if mode is GPGME_RANDOM_MODE_ZBASE32 bufsize needs to be at least 31 and will be filled with a string of 30 ASCII characters followed by a Nul; the remainder of the buffer is not changed. The caller must provide a context ctx initialized for GPGME_PROTOCOL_OPENPGP. This function has a limit of 1024 bytes to avoid accidental overuse of the random generator

I don't consider this a bug in gpgmepp's code. gpgmepp behaves exactly like gpgme (because it simply calls gpgme_op_random_bytes after creating a buffer of the requested size). With zbase32 you get 30 bytes zbase32 code and, if you requested more bytes, you get uninitialized additional bytes (which happen to be nullbytes, but that's more accidental than intentional). If anything then the problem is that gpgmepp's API is in general un(der)documented.

Granted that it might be more user-friendly for gpgmepp to return an error if the function is called with zbase32 and a count != 31.

I guess it would have been better gpgmepp API to add an additional function for getting 30 zbase32 bytes and to omit the mode flag in the generateRandomBytes function instead of mirroring the API of gpgme.

ikloecker claimed this task.
ikloecker triaged this task as Normal priority.

I have documented the function. The documentation is essentially a copy of the documentation of gpgme_op_random_bytes which should make clear that the function essentially behaves like gpgme_op_random_bytes (except that the gpgmepp function creates a buffer instead of taking one).

I'm open for adding a generateRandomZBase32 (or similar) which returns a std::string with 30 ZBase32 characters.

I don't think the trailing zero-byte should survive the conversion to c++ datastructures.

That's so surprising (and will lead to bugs and weirdnesses elsewhere)

If you loop over it, you process the zero byte. If you convert it to a string, you get a 31 byte string. If you compare it to a copy/paste version of itself it fails.

I explicitly keep the null byte so that you can simply pass the (pointer to the data of the) vector of bytes to the std::string c'tor. Meh! The c'tor wants const char *, but the vector is const unsigned char * so that one has to reinterpret_cast.

How embarrassing! The example usage in run-genrandom made exactly the error that you mention. I don't remember why I didn't use the std::string constructor for the conversion in the first place. I've changed this now.

Yes, it would have been better to not add the mode to generateRandomBytes and instead add the separate function. Unfortunately, I chose to stay too close to the gpgme API. I don't think that we can change it now without breaking API. A surplus null byte is a nuisance but removing a documented (! even if only for gpgme) terminating null byte from public API can cause out-of-bounds reads, crashes and worse problems. All we can do is add the separate function and deprecate the ZBase32 mode.

I came from it with stuffing the vector into a QByteArrayView - and then comparing it with the same string being roundtripped thru a copy/paste operation by the user.

It might be (putting it in the std::string ctor) how a c programmer would do it, I think using things like:

std::vector<unsigned char> c;
std::string s{c.begin(), c.end()};
std::string s2{std::from_range_t{}, c};

Is closer to how a c++ programmer would do it. All of it would be preserverve the nullptr and surprise the user.

I do also think upon second reading that we should deprecate the function entirely and make two different ones, getRandomBytes and getRandomZ32Bytes rather than try to overload them.

One of them return data where a nullbyte is valid data

The other of them is not having a nullbyte in valid data

One can be used directly to show the user if needed, the other one needs hex/base64/other-encoding and so on.