Skip to content

Masking and redaction

Refund untuk NIK ************1349, HP +62*******171, rekening ******7890.

Runs in your browser.

mask keeps NIK, NPWP, phone numbers and other identifiers out of logs, screens and support tickets, for example to meet the data minimization principle of Indonesia’s personal data protection law (UU PDP).

Go TypeScript Returns
mask.NIK(s) mask.nik(s) Last 4 digits visible
mask.NPWP(s) mask.npwp(s) Last 4 digits visible
mask.NIP(s) mask.nip(s) Last 4 digits visible
mask.NISN(s) mask.nisn(s) Last 4 digits visible
mask.Account(s) mask.account(s) Last 4 digits visible
mask.Phone(s) mask.phone(s) +62 and the last 3 digits visible
mask.Text(s) mask.text(s) Free text with every run of 10 or more digits masked
mask.Redact(v) mask.redact(v) A redacted deep copy of an object, array or value
mask.NewHandler(next) Go only A log/slog handler that redacts before next

The per-kind functions are the same as nik.Mask, phone.Mask and so on, collected in one place.

Text finds identifier-like numbers (10 or more digits, allowing the usual separators) and masks each one with the same rule as its kind: phone numbers keep +62 and 3 digits, everything else keeps 4. It scans in linear time and accepts up to 1 MiB (MaxText / MAX_TEXT). Longer input fails with length.

Redact returns a copy in which every string and key goes through Text (strings over 1 MiB become [redacted]) and every integer with 10 or more digits is masked. It never mutates the input, never calls getters (TypeScript returns [Getter]), skips __proto__, constructor and prototype keys, replaces cycles with [Circular], and stops at 32 levels ([MaxDepth]).

In Go, Redact accepts any value. Structs become maps keyed by their JSON names (fields tagged json:"-" and unexported fields are dropped), maps, slices, arrays and pointers are walked, []byte is treated as text, and time.Time is kept. Values that cannot be inspected, such as functions and channels, become [redacted] instead of passing through.

package main
import (
"encoding/json"
"fmt"
"github.com/haikalmumtaz233/nusaindex/mask"
)
func main() {
fmt.Println(mask.NIK("7502010706599583"))
fmt.Println(mask.NPWP("0029736758100600"))
fmt.Println(mask.Phone("0859-5257-171"))
fmt.Println(mask.NIP("198812272008092553"))
fmt.Println(mask.NISN("6299763315"))
fmt.Println(mask.Account("1234567890"))
line, err := mask.Text("customer 7502010706599583 called from 085952571710")
if err == nil {
fmt.Println(line)
}
record := map[string]any{
"name": "Budi",
"nik": "7502010706599583",
"notes": []any{"call 085952571710"},
}
safe, err := json.Marshal(mask.Redact(record))
if err == nil {
fmt.Println(string(safe))
}
}
Output
************9583
************0600
+62*******171
**************2553
******3315
******7890
customer ************9583 called from +62********710
{"name":"Budi","nik":"************9583","notes":["call +62********710"]}

NewHandler wraps any slog.Handler and redacts the message, every key and group, and every string, number and slog.Any value (through Redact) before passing the record on.

package main
import (
"log/slog"
"os"
"github.com/haikalmumtaz233/nusaindex/mask"
)
func main() {
plain := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
ReplaceAttr: func(_ []string, a slog.Attr) slog.Attr {
if a.Key == slog.TimeKey {
return slog.Attr{}
}
return a
},
})
logger := slog.New(mask.NewHandler(plain))
logger.Info("signup 7502010706599583", slog.String("phone", "085952571710"))
}
Output
level=INFO msg="signup ************9583" phone=+62********710