Is the following Unix command cryptographically secure to randomly generate 20 characters (a-zA-Z0-9 only)?

dd if=/dev/urandom bs=256 count=1 2> /dev/null | LC_ALL=C tr -dc 'A-Za-z0-9' | head -c20

Is there a better or more secure way to go about this in Unix?

  • I use unix.stackexchange.com/a/476125/9583 - you could easily grab 20 characters off the end for > 116.65 bits of entropy; less than the accepted answer, but with other nice properties as in my post. – Iiridayn Oct 17 '18 at 21:15

No, it's not entirely secure. Let's look at each of the commands:

dd if=/dev/urandom bs=256 count=1 2> /dev/null

This will read a single 256 byte block from /dev/urandom, a cryptographically secure random source. The problem starts here, and is related to this 256 byte limit. In fact, you do not even need to use dd here. The next command is perfectly able to read from a block device on its own.

LC_ALL=C tr -dc 'A-Za-z0-9'

This command strips out all characters that are not alphanumeric. The LC_ALL=C limits the charset to plain ASCII, where [:alnum:] will match 62 characters. Symbols and unprintable characters will be removed. The issue now is that only 256 bytes are given to this command, so what if too few of those bytes are alphanumeric? The tr command will happily take 256 bytes of input and spit out only a couple bytes of output if only a couple bytes match the filter.

head -c20

This command simply truncates the output to 20 bytes. The actual output will vary between 0 and 20 characters.* Statistically, it is most likely for it to output the entire 20 characters each time.

A superior way to get random alphanumeric characters can be done with fewer commands. This will have tr read as much data from /dev/urandom as it needs, continuously spitting out alphanumeric ASCII to the next command. As soon as head has the 20 bytes it has asked for, it will close the pipe, causing tr to stop reading random data and exit. This is what you should use:

LC_ALL=C tr -dc '[:alnum:]' < /dev/urandom | head -c20

This will guarantee 20 random characters, with an equivalent strength of log2(6220) ≈ 119.1 bits.

* The risk is largely theoretical. Each byte has a (256 - 62) / 256 chance (about 76%) of not being alphanumeric. The probability that at least 256 - 20 bytes are not alphanumeric is very low, but non-zero: (194 / 256)236 ≈ 3.8 × 10-28.

  • 1
    Awesome explanation and thanks for providing a superior solution. – Justin Apr 19 '18 at 3:16
  • Noticed when using random=$(LC_ALL=C tr -dc '[:alnum:]' < /dev/urandom | head -c20) in a bash script with set -eo pipefail; [[ $TRACE ]] && set -x causes the script to fail with a non-0 exit code. Any ideas why that is? – Justin Apr 19 '18 at 3:32
  • 1
    @Justin This happens because head closes the pipe as soon as 20 bytes have been read, which causes tr to exit with a broken pipe (after all, head is no longer listening). This is totally normal and expected behavior. When you use pipefail, that turns it into a fatal error. – forest Apr 19 '18 at 3:43
  • So if I use set -e; [[ $TRACE ]] && set -x instead that seems to work. – Justin Apr 19 '18 at 3:50
  • why not just use pwgen -s 20 1? – Shane Andrie Apr 19 '18 at 20:08

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.