Masking data against shoulder surfing

With BC27 we got a new option to mask field values, just like we have had for a long time, but there’s a new twist. Check out the video for all the details:

https://youtu.be/DoDnZi7y7A0


In this video, Erik explores a new feature introduced in Business Central 27 (BC27): the MaskType property. This property provides a built-in way to conceal sensitive field data on screen while still allowing users to reveal it when needed — a significant improvement over the traditional ExtendedDatatype = Masked approach. Erik also demonstrates custom masking logic for scenarios where you want partial data visibility, such as showing only the last four digits of a credit card number.

The Traditional Approach: ExtendedDatatype = Masked

Before BC27, if you wanted to hide a field’s value on a page, you would use the ExtendedDatatype property set to Masked. This renders the field value as dots (or asterisks), and there is no way for the user to reveal the actual value through the UI.

Erik demonstrates this by adding a field to the Customer Card and setting:

ExtendedDatatype = Masked;

The result: the field shows dots when you type into it, and even the page inspection tool shows asterisks instead of the real value. This is ideal for data that should never be visible to the user — passwords, API keys, registration tokens, and similar secrets.

But what if the data is sensitive yet users sometimes need to see it? Previously, developers had to resort to workarounds like adding an AssistEdit trigger that displayed the value via a Message call — functional, but far from elegant.

The New Approach: MaskType = Concealed (BC27)

With BC27, Microsoft introduced a new page field property called MaskType. It accepts two values:

  • None — the field behaves normally (as if the property isn’t set)
  • Concealed — the field value is hidden behind dots, but with a key difference: a small eye icon (with a slash through it) appears next to the field

Clicking the eye icon reveals the actual value. Clicking it again re-conceals it. This is a built-in toggle that requires no custom code.

Key Differences from ExtendedDatatype = Masked

  1. The dot count differs: With MaskType = Concealed, the number of dots displayed does not necessarily correspond to the actual character count of the value. This adds an extra layer of obfuscation — an observer can’t even deduce the length of the data.
  2. Reveal toggle: The eye icon lets users see and then re-hide the value at will.
  3. Page inspection still shows the real value: Unlike ExtendedDatatype = Masked, where page inspection also shows asterisks, the MaskType = Concealed approach does expose the actual value in the page inspection pane. This is an important distinction to be aware of.

When to Use Which

Scenario Recommended Property
Passwords, API keys — never visible ExtendedDatatype = Masked
Social security numbers, sensitive IDs — sometimes visible MaskType = Concealed

Erik notes that this feature is particularly useful in “semi-public” scenarios — for example, a point-of-sale-like setup where a Business Central screen is partially visible to bystanders and you want to protect sensitive information from shoulder surfing.

No Timeout (Yet)

Erik observes that once you click the eye icon to reveal the value, it stays visible indefinitely until you click the icon again. He suggests that an auto-hide timeout (e.g., 10 or 30 seconds) would be a welcome enhancement. For now, users need to remember to re-conceal the field manually.

Limitation: Cannot Add MaskType to Existing Fields via Page Extensions

Erik tries an interesting experiment: can you add MaskType = Concealed to an existing base application field (like the Balance field on the Customer Card) through a page extension? The answer is no. Business Central throws an error:

“Modifications to property MaskType are not allowed.”

This means you cannot retroactively conceal fields defined in the base application or other extensions. Erik argues that Microsoft should at least allow increasing the concealment level (from none to concealed), even if reducing it should be blocked. As it stands, the workaround would be to hide the original field and add your own concealed version — not ideal, but functional.

Custom Masking: Showing the Last Four Digits

For scenarios where you want more control over what’s displayed — such as showing only the last four digits of a credit card number — you can implement custom masking logic. The source code provided demonstrates this pattern:

Table Extension: Adding the Field

tableextension 50100 MyCustomer extends Customer
{
    fields
    {
        field(50100; CreditCardNo; Text[50])
        {
            Caption = 'Credit Card No.';
        }
    }
}

This adds a CreditCardNo field to the Customer table to store the full credit card number.

Page Extension: Custom Masking Logic

pageextension 50100 MyCustomer extends "Customer Card"
{
    layout
    {
        addafter(Name)
        {
            field(CreditCardCtl; MaskedCreditCard)
            {
                Caption = 'Credit Card No.';
                ApplicationArea = all;
                trigger OnValidate()
                begin
                    if strlen(MaskedCreditCard) < 5 then
                        error('Not a valid credit card');
                    Rec.CreditCardNo := MaskedCreditCard;
                    MaskedCreditCard := PerformMasking();
                end;
            }
        }
    }
    trigger OnAfterGetRecord()
    begin
        MaskedCreditCard := PerformMasking();
    end;

    trigger OnAfterGetCurrRecord()
    begin
        MaskedCreditCard := PerformMasking()
    end;

    procedure PerformMasking(): Text
    begin
        if Strlen(Rec.CreditCardNo) > 4 then
            exit('*************' + Rec.CreditCardNo.Substring(strlen(Rec.CreditCardNo) - 3))
        else
            exit('');
    end;

    var
        MaskedCreditCard: Text[50];
}

Here’s how this works:

  • The page control is bound to a variable (MaskedCreditCard) rather than directly to the table field. This gives you full control over what’s displayed.
  • The PerformMasking() procedure replaces all but the last four characters with asterisks — a common pattern for credit card display (e.g., *************1234).
  • On OnAfterGetRecord and OnAfterGetCurrRecord, the masked version is computed and displayed.
  • On OnValidate, the full value entered by the user is saved to Rec.CreditCardNo, and the display is immediately re-masked.
  • A basic validation check ensures the input is at least 5 characters long.

This approach gives you the flexibility to define exactly how masking works — partial reveal, custom asterisk patterns, different rules for different fields — while keeping the actual sensitive data stored securely in the table field.

Summary

BC27 introduces the MaskType property as a welcome addition to the developer’s toolkit for protecting sensitive on-screen data:

  • ExtendedDatatype = Masked remains the right choice for data that should never be visible (passwords, API keys).
  • MaskType = Concealed is perfect for data that is sensitive but occasionally needs to be viewed (social security numbers, account identifiers), providing a built-in reveal/hide toggle.
  • Custom masking logic (as shown in the credit card example) is ideal when you want partial visibility or specific formatting rules.
  • Be aware that MaskType = Concealed does not hide values from page inspection, and you currently cannot apply it to existing base application fields via page extensions.

This is not about computer security in the traditional sense — it’s about physical security, protecting against shoulder surfing in environments where screens may be partially visible to unauthorized observers.