Since you're using Bash, you can use
Code:
typeset +x plainpwd
read -p 'Password: ' -s plainpwd
The typeset +x means that
plainpwd should not be automatically exported as an environment variable. If you are careful to keep
plainpwd as Bash variable, never export it to the environment, and never use it as a command line parameter to a non-builtin command, the password can be considered relatively safe.
(In particular, processes belonging to the same user can still sniff the password by reading /proc/
pid/mem, the memory belonging to the Bash process. But it is not directly and easily accessible.)
Character codes 32..126 are ASCII printable letters. Codes 0..31 and 127 are control characters. Since on all Linux architectures bytes are eight bits wide, but the signedness of the non-ASCII characters varies, you should just AND with the 8-bit mask (
code & 255) to make sure the non-ASCII characters occupy codes 128..255; this is the standard convention. The interpretation of codes 128..255 == -128..-1 depend on the character set used. In particular, when using UTF-8, many characters consist of
multiple bytes in that range.
A nifty way to convert the password -- or indeed any other string -- to an array of prefixless hex numbers is via od:
Code:
export LC_ALL=C LANG=C
hex=($(builtin echo -n "$plainpwd" | od --width=${#plainpwd} -t x1 | sed -ne '1 s|^[0-9A-Fa-f]* ||p'))
Or, to a big-endian hexadecimal number,
Code:
export LC_ALL=C LANG=C
hex=$(builtin echo -n "$plainpwd" | od --width=${#plainpwd} -t x1 | sed -ne '1 s|^[0-9A-Fa-f]*||p' | tr -cd '0-9A-Fa-f')
You do need to set LC_ALL and LANG to C, because in UTF-8 locales the byte count of a string and the character count of a string do not match. For example, in UTF-8, € = e2 82 ac, i.e. three bytes, but one character. If you really don't want to do that, you can use
--width=$[4*${#plainpwd}] instead; this makes sure that od outputs all the hex bytes on a single line. (Currently an UTF-8 character may need up to four bytes, although full UCS support requires up to six bytes per character.)
The
builtin keyword makes sure the echo is done by the Bash shell itself, and that it will never execute an external program with the plain password as its parameter. This ensures the plain password is never used directly as a command-line argument, and is thus not leaked to the process list.
As you probably know, all command arguments are visible in the process list, e.g. via
ps -e -o pid,comm,args
Edited to add:
Please also consider salting your hashes. Instead of just doing (mathematically)
Code:
result = hash(plaintext)
do
Code:
salt = random number or string, typically 8 - 16 characters
result = salt + optional_separator_1 + hash(salt + optional_separator_2 + plaintext)
The result of this salting is that you can no longer generate a large dictionary of hashes of candidate passwords, and just compare to your hash. Each different salt would need a new dictionary!
For example:
Code:
salt=$(dd if=/dev/urandom bs=16 count=1 of=/dev/stdout 2>/dev/null | od --width=16 -t x1 | sed -ne '1 s|^[^ ]* ||p' | tr -cd '0-9A-Fa-f')
secret="this is the secret text"
hash=$(echo -n "$salt|$secret" | sha256sum -b | sed -ne '1 s| .*$||p')
result="$salt|$hash"
echo "Result: $result"
produces a different hash every time. To check if a response matches the secret, use
Code:
read -p 'Response: ' -s response
salt="${result%%|*}"
hash=$(echo -n "$salt|$response" | sha256sum -b | sed -ne '1 s| .*$||p')
[ "$result" = "$salt|$hash" ] && echo "Matches" || echo "Does not match"