10

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?

1
  • 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
21

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.

You can also turn this into a shell script function to make password generation from command line easier. This particular function takes the number of characters for the password as an argument. If no argument is specified, it defaults to 20 characters:

newpass() {
    LC_ALL=C tr -dc '[:alnum:]' < /dev/urandom | head -c${1:-20}
}

* 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.

14
  • 2
    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
  • 2
    @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
  • 1
    @ShaneAndrie The SUS is the Single Unix Specification, a standard for all *nix operating systems. XCU is specifically the utilities that must be pre-installed on the system. – forest Apr 24 '18 at 23:10

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.