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:
- Transformer Stage with custom routines
- External library for hashing (often COBOL or Java)
- 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
| Expression | What 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)
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:
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:
# Document your hash spec:
# Algorithm: SHA-256
# Salt: env("HASH_SALT")
# Encoding: hexadecimal before truncation
# Truncation: 16 characters
# Input: Raw account number with dashes