Skip to main content

Step 3: Mask Account Numbers

Reduce account-number exposure with an authored mask and deterministic join key. This page compares the mapping with a DataStage-style transformer.

The Goal​

  • Remove the full account number from the record
  • Keep last 4 digits for customer service reconciliation
  • Create a hash for joining related records without exposing the actual number

Why This Matters​

Security boundary: Tokenization, keying, access, retention, and regulatory controls require a separate design.

Analytics Need: You still need to count unique accounts and join transactions.

The Solution: Hash for joins, last-4 for display.

DataStage Equivalent​

In DataStage, account masking typically requires:

  1. Transformer Stage with custom routines
  2. External library for hashing (often COBOL or Java)
  3. Multiple output columns managed manually

Expanso simplifies this with built-in slice() and hash() functions.

Implementation​

See the masking processor in the canonical reference and its captured input and output in the Explorer.

Understanding the Code​

ExpressionWhat It Does
.slice(-4)Get last 4 characters
.hash("sha256")Create SHA-256 digest bytes
.encode("hex")Encode digest bytes as hexadecimal text
.slice(0, 16)Keep 16 hexadecimal characters
.without("ACCOUNT_NUMBER")Remove field from record

Expected Output​

Use the Explorer checkpoints for the sample input and resulting fields.

Production Considerations​

Salted Hashing​

Add a secret salt to prevent rainbow table attacks:

root.account_number_hash = (env("HASH_SALT") + this.ACCOUNT_NUMBER).hash("sha256").encode("hex").slice(0, 16)
Protect Your Salt

Store the hash salt in a secrets manager. If compromised, attackers can reverse-engineer account numbers.

Format-Preserving Masking​

For systems that validate account format:

# Preserve format: XXXX-XXXX-XXXX-1234
let parts = this.ACCOUNT_NUMBER.split("-")
root.account_number_masked = "XXXX-XXXX-XXXX-" + $parts.index(3)

Multiple Sensitive Fields​

Mask several fields at once:

Partial snippet: multiple-field masking mapping
root = root.without("ACCOUNT_NUMBER", "CARD_CVV", "CARD_EXPIRY")
root.account_last4 = this.ACCOUNT_NUMBER.slice(-4)
# Boolean flag only
root.card_present = this.CARD_CVV != null

Consistent Hashing Across Pipelines​

Ensure all pipelines use the same hash configuration for joins to work:

Reference notes (not pipeline YAML)
# Document your hash spec:
# Algorithm: SHA-256
# Salt: env("HASH_SALT")
# Encoding: hexadecimal before truncation
# Truncation: 16 characters
# Input: Raw account number with dashes

Next Step​