Protect your secret (texts) against prying eyes and debuggers!

There are many secrets in Business Central, and some of them can be hard to keep secret. Introducing SecretText, a way to keep certain values away from prying eyes. Check out the video:

https://youtu.be/ljW7XckeMw8

In this video, Erik explores the SecretText data type in Business Central — a relatively recent addition designed to prevent sensitive information like API keys, passwords, and tokens from being exposed through the debugger. He demonstrates the problem with storing secrets in regular text variables and shows how SecretText keeps values hidden, even during debugging sessions.

The Problem: Secrets Exposed in the Debugger

Erik starts by setting up a simple scenario where a secret value is retrieved from isolated storage and stored in a regular Text variable. Isolated storage ensures that only the app itself can read its own stored values — other apps cannot retrieve them. However, there’s a significant problem: anyone who can debug the extension can see the secret value in plain text.

When you step through the code with the debugger, the variable’s value is fully visible. This is a real security concern, especially in multi-tenant cloud environments where delegated admins or partners may have debugging access.

One workaround is to mark procedures as NonDebuggable, but this creates a frustrating situation. You end up making entire procedures non-debuggable just to protect a single variable, which makes troubleshooting legitimate issues much harder.

Enter SecretText

The SecretText data type solves this elegantly. Instead of declaring your variable as Text, you declare it as SecretText. When you debug code that uses a SecretText variable, the debugger shows “Hidden value” instead of the actual content — no matter where you inspect it.

Erik demonstrates that the hidden value protection follows the secret everywhere:

  • In the procedure where it’s first assigned
  • When passed as a parameter to other functions
  • When inspected in watch windows or variable tooltips
  • When added to HTTP headers using the secret-aware overloads

HTTP Client Integration

The AL HTTP client types have been updated to accept SecretText values directly. For example, when adding authorization headers, you can use overloads that accept SecretText parameters, so the secret never needs to be converted to plain text:

// The HTTP client has overloads that accept SecretText
// so you never need to unwrap the secret
client.DefaultRequestHeaders().Add('Authorization', mySecretToken);

Preventing Secret Spillover

One of the key design principles of SecretText is that you cannot accidentally spill a secret into an unprotected variable. If you try to assign a SecretText value to a regular Text variable, the compiler will block it:

// This will NOT compile — by design
var
    t2: Text;
    t: SecretText;
begin
    t2 := t; // Compiler error: cannot convert SecretText to Text
end;

This prevents developers from inadvertently extracting secrets and storing them in variables that the debugger can expose. On-premises installations do have an Unwrap method available, but in the cloud, you’re intentionally restricted from converting SecretText back to plain text.

Creating SecretText Values in Code

You can create SecretText values programmatically. While you can’t directly assign a text literal to a SecretText variable, you can use the format function or return a SecretText from a NonDebuggable procedure:

// A non-debuggable procedure that generates a secret
[NonDebuggable]
procedure GenerateSecret(): SecretText
begin
    exit('some-secret-value');
end;

When you call this function from debuggable code, the debugger will skip right over the non-debuggable procedure, and the returned value will show as “Hidden value” in the calling code. This pattern gives you a clean way to create secrets while keeping the rest of your code fully debuggable.

A Word on Events and Secrets

Erik also explores whether you can pass SecretText through integration events. While the compiler does allow it, he cautions against this practice. Exposing secrets through events means subscribers — potentially from other extensions — could receive the secret value. Even though the SecretText type provides debugger protection, broadcasting secrets through events undermines the principle of keeping sensitive data contained.

Related: Field Masking for Sensitive Data on Pages

While SecretText protects secrets at the code level, you may also need to mask sensitive data displayed on pages. The provided source code demonstrates a complementary technique — masking a credit card number on the Customer Card page:

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

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

This approach stores the full credit card number in the database but only displays the last four digits on the page, prefixed with asterisks. The field is bound to a page variable (MaskedCreditCard) rather than directly to the record field, so the masking logic runs on every record retrieval. Note that this is UI-level masking — for true security of secrets like API keys, the SecretText data type is the proper solution.

Summary

The SecretText data type is an important security feature for Business Central developers. Here are the key takeaways:

  • Use SecretText for any sensitive values like API keys, passwords, and tokens
  • The debugger cannot reveal SecretText values — they always show as “Hidden value”
  • You cannot assign a SecretText to a regular Text variable, preventing accidental spillover
  • HTTP client methods have overloads that accept SecretText directly
  • Use NonDebuggable procedures to generate or initialize secret values
  • Avoid exposing secrets through events — keep them contained within your app
  • On-premises has an Unwrap method; cloud environments intentionally restrict this
  • For UI-level protection, consider field masking patterns to hide sensitive data on pages