16

I need to print some variables to the screen but I need to preferebly obfuscate the first few characters and I was wondering if there was an echo command in bash that can obfuscate the first characters of a secret value while printing it to the terminal:

echo 'secretvalue'
********lue
Jeff Schaller
  • 67,283
  • 35
  • 116
  • 255
xerxes
  • 333

7 Answers7

15

One option would be to force yourself to use a function instead of echo, such as:

obfuprint() {
  if [ "${#1}" -ge 8 ]
  then
    printf '%s\n' "${1/????????/********}"
  else
    printf '%s\n' "${1//?/*}"
  fi
}

Then you could call obfuprint 'secretvalue' and receive ********lue (with a trailing newline). The function uses parameter expansion to search for the first eight characters of the passed-in value and replaces them with eight asterisks. If the incoming value is shorter than eight characters, they are all replaced with asterisks. Thanks to ilkkachu for pointing out my initial assumption of eight-or-more character inputs!


Inspired by ilkkachu's flexible masking answer, I thought it'd be interesting to add a variation that randomly masks some percentage of the string:

obfuprintperc () {
  local perc=75  ## percent to obfuscate
  local i=0
  for((i=0; i < ${#1}; i++))
  do
    if [ $(( $RANDOM % 100 )) -lt "$perc" ]
    then
        printf '%s' '*'
    else
        printf '%s' "${1:i:1}"
    fi
  done
  echo
}

This relies on bash's $RANDOM special variable; it simply loops through each character of the input and decides whether to mask that character or print it. Sample output:

$ obfuprintperc 0123456789
0*****6*8*
$ obfuprintperc 0123456789
012***678*
$ obfuprintperc 0123456789
**********
$ obfuprintperc 0123456789
*****56***
$ obfuprintperc 0123456789
0*******8*
Jeff Schaller
  • 67,283
  • 35
  • 116
  • 255
  • 1
    To be frank, I do not like random masking. A determined shoulder surfer will eventually get my secrets by pretending to like making small talk with me. – emory Mar 20 '19 at 17:30
  • 1
    Certainly, displaying sensitive information should be done carefully! I presented random masking as an alternative to fixed-prefix masking and variable-prefix masking. – Jeff Schaller Mar 20 '19 at 17:31
  • 4
    I am not a fan of fixed-prefix or variable-prefix masking either, but with those there exists a "kernel" of my secret that remains secret. With random masking, there is no "kernel". Eventually everything will be revealed to those patient enough. – emory Mar 20 '19 at 17:35
13

The other answers mask a fixed amount of characters from the start, with the plaintext suffix varying in length. An alternative would be to leave a fixed amount of characters in plaintext, and to vary the length of the masked part. I don't know which one is more useful, but here's the other choice:

#!/bin/bash
mask() {
        local n=3                    # number of chars to leave
        local a="${1:0:${#1}-n}"     # take all but the last n chars
        local b="${1:${#1}-n}"       # take the final n chars 
        printf "%s%s\n" "${a//?/*}" "$b"   # substitute a with asterisks
}

mask abcde
mask abcdefghijkl

This prints **cde and *********jkl.


If you like, you could also modify n for short strings to make sure a majority of the string gets masked. E.g. this would make sure at least three characters are masked even for short strings. (so abcde -> ***de, and abc -> ***):

mask() {
        local n=3
        [[ ${#1} -le 5 ]] && n=$(( ${#1} - 3 ))
        local a="${1:0:${#1}-n}"
        local b="${1:${#1}-n}"
        printf "%s%s\n" "${a//?/*}" "$b"
}
ilkkachu
  • 138,973
9

A zsh variant that masks three quarters of the text:

mask() printf '%s\n' ${(l[$#1][*])1:$#1*3/4}

Example:

$ mask secretvalue
********lue
$ mask 12345678
******78
$ mask 1234
***4

To mask the first 8 chars:

mask() printf '%s\n' ${(l[$#1][*])1:8}

To mask all but the last 3 chars:

mask() printf '%s\n' ${(l[$#1][*])1: -3}

To mask a random number of characters:

mask() printf '%s\n' ${(l[$#1][*])1: RANDOM%$#1}
8

You could try piping to sed. For example, to replace the first 8 characters of a string with asterisks, you could pipe to the sed 's/^......../********/' command, e.g.:

$ echo 'secretvalue' | sed 's/^......../********/'
********lue

You can also define a function that does this:

obsecho () { echo "$1" | sed 's/^......../*********/'; }
igal
  • 9,886
2

Another option in Bash, if you don’t mind one simple eval you can do it with a couple of printf:

# example data
password=secretvalue
chars_to_show=3

# the real thing
eval "printf '*%.0s' {1..$((${#password} - chars_to_show))}"
printf '%s\n' "${password: -chars_to_show}"

But be careful:

  • fix the above as you need when ${#password} is less than ${chars_to_show}
  • eval can be very dangerous with untrusted input: here it can be considered safe because its input comes only from safe sources, ie the length of ${password} and the value of ${chars_to_show}
LL3
  • 5,418
1
$ echo "helloworld" | sed -E "s/(.{3})(.{5})/\1*****/"
hel*****ld

(.{3}) - the masking starts after the 3rd letter
(.{5}) - the quantity of letters that will be replaced by *****. (.{255}) will be the maximum allowed on the MacOS BSD sed.

sed -E can be changed with sed -r, but sed -E is more portable across shells.

t7e
  • 323
0

Here's some toy Bash scripts to play with that show how to combine regex-like search with string substitution.

strip_str.sh

#!/usr/bin/env bash

_str="${1}"
_filter="${2:-'apl'}"
echo "${_str//[${_filter}]/}"
strip_str.sh 'apple-foo bar'
# -> e-foo br
strip_str.sh 'apple-foo bar' 'a'
# -> pple-foo br

privatize_str.sh

#!/usr/bin/env bash

_str="${1}"
_filter="${2:-'apl'}"
_replace="${3:-'*'}"
echo "${_str//[${_filter}]/${_replace}}"
privatize_str.sh 'apple-foo bar'
# -> ****e-foo b*r

restricted_str.sh

#!/usr/bin/env bash

_str="${1}"
_valid="${2:-'a-z'}"
_replace="${3:-''}"
echo "${_str//[^${_valid}]/${_replace}}"
restricted_str.sh 'apple-foo bar'
# -> applefoobar

Key takeaways

  • [a-z 0-9] is totally valid, and handy, as a <search> within ${_var_name//<search>/<replace>} for Bash
  • ^, within this context, is the reverse or not for regex-like searches
  • Built-ins are generally faster and often are more concise, especially when it cuts out unneeded piping

While I get that printf is better in nearly all use cases the above code uses echo so as to not overly confuse what's going on.

obfuscate_str.sh

#!/usr/bin/env bash

_str="${1}"
_start="${2:-6}"
_header="$(for i in {1..${_start}}; do echo -n '*'; done)"
echo "${_header}${_str:${_start}}"
obfuscate_str.sh 'apple-foo bar' 3
# -> ***le-foo bar
S0AndS0
  • 456