11

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?

Justin
  • 1,117
  • 3
  • 14
  • 20
  • I use https://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

1 Answers1

24

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.

forest
  • 64,616
  • 20
  • 206
  • 257
  • 2
    Awesome explanation and thanks for providing a superior solution. – Justin Apr 19 '18 at 03: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 03: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 03:43
  • So if I use `set -e; [[ $TRACE ]] && set -x` instead that seems to work. – Justin Apr 19 '18 at 03:50
  • why not just use pwgen -s 20 1? – Shane Andrie Apr 19 '18 at 20:08
  • @ShaneAndrie SUS XCU7 does not specify `pwgen`, but it does specify `tr` and `head`. – forest Apr 20 '18 at 02:46
  • @forest Can you explain SUS XCU7 for reference? – Shane Andrie Apr 24 '18 at 15:49
  • 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
  • The suggested commands didn't work in my case. They just returned with no output. I used this version instead: `< /dev/random stdbuf -o0 strings --bytes 1 | stdbuf -o0 tr -d '\n\t '` – rubik Aug 02 '18 at 12:15
  • @rubik What shell are you using? It works correctly for me using bash and ash.w – forest Aug 03 '18 at 06:38
  • @forest I use Zsh, maybe that is the reason. – rubik Aug 03 '18 at 07:05
  • @rubik Have you tried replacing `[:alnum:]` with `a-zA-Z0-9`? That is necessary for some shells. – forest Aug 03 '18 at 07:06
  • @forest Yep, does not work. For some reason the command always returns with no output. – rubik Aug 04 '18 at 06:28
  • @rubik That's very odd. Try `set -x; tr -cd 'a-zA-Z0-9' < /dev/urandom | head -c20` to view the debugging output. The command itself is very simple, so I can't imagine what could be going wrong. As long as you have a readable random device and shell redirections are supported, this should work. The `tr` and `head` commands' behavior are part of a strict standard. – forest Aug 04 '18 at 06:29