1

I am trying to build a random character generator in a bash script on osx 10.8.5 . The goal is to generate random character strings for a script generating salts for the wordpress wp-config.php file. The snippet looks like the following:

#!/bin/bash -e
read -p "Number of digits: " digits

function rand_char {
    take=$(($RANDOM % 88)); i=0; echo {a..z} {A..Z} {0..9} \, \; \. \: \- \_ \# \* \+ \~ \! \§ \$ \% \& \( \) \= \? \{ \[ \] \} \| \> \< | while read -d\  char;
    do
        [ "$i" = "$take" ] && echo "$char\c";
        ((i++));
    done
}

function rand_string {
    c=$1;
    while [ $c -gt 0 ];
        do char="${char}"$(rand_char);
        let c=$c-1;
    done
    echo $char
}

outputsalt=`rand_string $digits`

echo $outputsalt

if i enter 64 as the number of digits the resulting number of characters differs each try:

HeF6D>z}x[v=s(qRoPmNkLiIfG7E5C3A1yZwWtU§S~Q*O_M:J,b86|4]2{0)X&p    (63 chars)
WtUrSpQnOkLiJ,H8F6D4B1yZ)X&V$T!R+P_M:e;c9a7>5}2{u=s(q%o§m~j#hIfG   (64 chars)
_g:d,b86|4]w{u=r&p$n!lMjKhIeFcDaB>z0xYt)r&p§m~kLiJgHeFcDA1yZwX     (62 chars)
}w{u=s(q%oPmNkKhIfGdEbC3A1xYvWtUrS~Q*O_L.J,H8F6|4]2?Z)X&V$n!l+j_   (64 chars)
l+j_g:e;cDaB>z}x{u=sTqRoPmNkKhIfG7E5C3A1xYvW%U§S~Q*O_L.J,b86|4]    (63 chars)

Is there a way that the number of characters sticks to the given number? Best regards Ralf

Barmar
  • 741,623
  • 53
  • 500
  • 612
rkoller
  • 1,424
  • 3
  • 26
  • 39

4 Answers4

5

Try a simpler case:

function rand_char {
  take=$(($RANDOM % 2)); i=0; echo a b | while read -d\  char;
  do
    [ "$i" = "$take" ] && echo "$char\c";
    ((i++));
  done
}

It will produce a and blanks, but no b.

We can further reduce the problem down to:

echo a b | while read -d\  char; do echo "$char"; done

which only writes a and not b. This is because you instruct read to read up to a space, and there's no space after b so it fails. This means that one out of every 88 chars will be dropped, causing your lines to be slightly shorter.

The simplest fix is to add a dummy argument to force a space at the end:

echo {a..z} {A..Z} {0..9} (etc etc) \} \| \> \< '' | while read ...
#                                       Here ---^

Note that your method just adds 15 bits of entropy to the salt, while the absolute minimum should be 64. The significantly easier and more secure way of doing this would be:

LC_CTYPE=C tr -cd 'a-zA-Z0-9,;.:_#*+~!@$%&()=?{[]}|><-' < /dev/urandom | head -c 64

(note: this replaces your unicode paragraph symbol with an ascii @)

that other guy
  • 116,971
  • 11
  • 170
  • 194
  • +1 I noticed that it was failing to add a character when `take=87`, but I couldn't figure out the reason like you did. – Barmar May 23 '14 at 20:09
  • 3
    The `tr`-based command is a great alternative, but (at least on OSX) it will fail with `illegal byte sequence` unless you prefix it with `LC_CTYPE=C` (or, more robustly, `LC_ALL=C`): `LC_ALL=C tr -cd 'a-zA-Z0-9,;.:_#*+~!@$%&()=?{[]}|><-' < /dev/urandom | head -c 64` – mklement0 May 23 '14 at 20:23
  • 1
    And also thanks to @mklement0 for the comment about OSX workaround. Exactly ran into that ;) – rkoller May 23 '14 at 20:40
4

No offense, by your coding style is, errr..., not the best I've seen :).

#!/bin/bash -e

read -p "Number of digits: " digits
# TODO: test that digits is really a number

chars=( {a..z} {A..Z} {0..9} \, \; \. \: \- \_ \# \* \+ \~ \! \§ \$ \% \& \( \) \= \? \{ \[ \] \} \| \> \< )

function rand_string {
    local c=$1 ret=
    while((c--)); do
        ret+=${chars[$((RANDOM%${#chars[@]}))]}
    done
    printf '%s\n' "$ret"
}

outputsalt=$(rand_string $digits)

echo "$outputsalt"
gniourf_gniourf
  • 44,650
  • 9
  • 93
  • 104
  • 1
    I admit "not the best" is still way too euphemistic. It is more or less my first take on bash scripting. In my first iteration i try to get things working basically. Still space to improve for me ;) Thanks for a more elegant take on the problem :) – rkoller May 23 '14 at 20:37
1

I see two problems in your script.

I'm pretty sure

take=$(($RANDOM % 88));

should be

take=$(($RANDOM % 87));

Otherwise, it appears you're going past the end of your input stream.

The other problem is this char:

Bash is seeing that as two characters (wide char?). I'd delete it from your possibilities.

Of course, that would mean the above line would be:

take=$(($RANDOM % 86));

Doing those two things, in fact, works for me.

Edited:

@that other guy has a better answer. Adding the space rather than reducing the modulo will ensure that you get every character

SeeJayBee
  • 1,188
  • 1
  • 8
  • 22
  • As for `§` (the section sign): you're correct; specifically: its Unicode codepoint is `0xa7`, outside the 7-bit ASCII range; in UTF8 encoding (as is the default on OSX), you get *2* bytes, namely `0xc2` and `0xa7`. – mklement0 May 23 '14 at 20:07
0

Another way of doing this if you fancy one-liners:

perl -le 'print map { ("a".."z","A".."Z",0..9,",",";",".",":","-","_","#","*","+","~","!","§","\$","%","&","(",")","=","?","{","}","[","]","|","<",">") [rand 87] } 1..63'

or, as you probably won't want a new line at end:

perl -e 'print map { ("a".."z","A".."Z",0..9,",",";",".",":","-","_","#","*","+","~","!","§","\$","%","&","(",")","=","?","{","}","[","]","|","<",">") [rand 87] } 1..63'
Tiago Lopo
  • 7,619
  • 1
  • 30
  • 51