Masking Sensitive Information in FreshMarker

Working as a software developer allows me to constantly look into new subject areas and gather inspiration for my projects. Looking at a document with redacted information was the impetus for two new FreshMarker built-ins mask and mask_full.

If you do not want to disclose some information in a message, but want to give the addressee sufficient information, you can hide a large part of this information. For example, to give the addressee a known telephone number, it is not necessary to include the entire telephone number. The last three digits are sufficient here. So instead of +49 176 04069042, ●●● ●●● ●●●●●042 is sufficient in the message. As the addressee knows his telephone number, which ends with the digits 042, the information has been delivered without revealing all the details to third parties.

The following examples show how the functionality is to be integrated into FreshMarker as new built-ins.

${'secret phrase'?mask}
${'secret phrase'?mask_full('#')}
${'5555 5555 5555 4444'?mask_full('•●⬤●', 2)}
${'schegge.de'?mask_full('░', 3)}
${'5555 5555 5555 4444'?mask(2)}
***** ******
########
•●⬤●•●⬤●•●⬤●•●⬤●•●44
░░░░░░░.de
**** **** **** **44
########

Without parameters, the built-in mask replaces all characters that are not spaces. The built-in mask_full replaces all characters, including spaces. The difference between the two variants is that mask still reflects the structure of the information and mask_full hides it. With a string parameter, this is used for masking. If it is longer than one character, it is applied cyclically, as in the padding implementation. A further numeric parameter specifies how many characters should not be masked at the end.

The built-ins are registered to FreshMarker via the Extension API and both use the same implementation method mask.

register.add("mask", (x, y, e) -> mask((TemplateString) x, e, y, false));
register.add("mask_full", (x, y, e) -> mask((TemplateString) x, e, y, true));

The mask method by itself is quite trivial. It evaluates its parameters and uses String#repeat for the simple cases and a for loop for the main implementation.

private TemplateString mask(TemplateString value, ProcessContext context, List<TemplateObject> parameters, boolean full) {
  String string = value.getValue();
  if (string.isEmpty()) {
    return value;
  }
  if (full && parameters.isEmpty()) {
    return new TemplateString("*".repeat(string.length()));
  }
  String maskPattern = "*";
  int firstIndex = 0;
  if (!parameters.isEmpty() && parameters.getFirst().evaluateToObject(context) instanceof TemplateString first) {
    maskPattern = first.getValue().isEmpty() ? "*" : first.getValue();
    firstIndex = 1;
  }
  StringBuilder builder = new StringBuilder(string);
  int unmaskCharacters = parameters.size() > firstIndex? parameters.get(firstIndex).evaluate(context, TemplateNumber.class).asInt() : 0;
  for (int i = 0; i < string.length() - unmaskCharacters; i++) {
    if (full || builder.charAt(i) != ' ') {
      builder.setCharAt(i, maskPattern.charAt(i % maskPattern.length()));
    }
  }
  return new TemplateString(builder.toString());
}

If a user has more specific requirements for a custom mask implementation, then this example can be used as a starting point. Using the Extension API, custom implementations for special use cases can be implemented just as easily as the built-ins presented here. These built-ins will be part of the next FreshMarker version, so please be patient.

Leave a Comment