14

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
11

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"
}
13

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*
  • 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 at 17:30
  • 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 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 at 17:35
7

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/^......../*********/'; }
  • 2
    I'd suggest printf over echo so that you're not subject to interpreting data such as \r or \n – Jeff Schaller Mar 20 at 14:27
  • @JeffSchaller This is one of the reasons why I post on SE. Good point. Thanks for the feedback. – igal Mar 20 at 14:29
  • It's one of the many things I've learned in my time here, as well! Happy to pass it along! – Jeff Schaller Mar 20 at 14:30
  • 1
    No need to use a pipe when you can use a herestring instead: sed 's/^......../********/' <<< 'secretvalue' – wjandrea Mar 20 at 15:24
  • @roaima It's actually a temporary regular file. You can see it if you do bash -c 'lsof -d0 -a -p $$ 2>/dev/null' <<< foo. – JoL Mar 20 at 15:35
7

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}
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}
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

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.