v0.1.0-beta.1 · Apache-2.0 · Java 8+ · three engines, one facade

ns-secure
Secrets never reach your logs.

A lightweight Java security framework behind one facade — NS. Three schema-driven engines: a masking log engine, a field-level crypto engine (AES-GCM, RSA-OAEP, HMAC, PBKDF2) and a unique-id engine (ULID, UUIDv7, SNOWFLAKE) — all discovered from the classpath.

io.github.nsprism:ns-secure:0.1.0-beta.1
What is ns-secure

One facade. Every security concern.

ns-secure is a unified security facade for the JVM. Application code talks to a single entry point — NS — and never to a provider directly. Which engine answers is decided by the operation, the schema's type, and what is on your classpath: implementations register through java.util.ServiceLoader, so adding a module is the only wiring there is.

Every schema is a versioned JSON resource with a declared kind — LOG, GENERATE or CRYPTO/ENCRYPT family. The log engine masks events deny-by-default; the id engine fills empty id fields with ULIDs, UUIDv7s or custom-patterned keys; the crypto engine encrypts, hashes or drops individual fields, binding every ciphertext to its <schema>/<field> context so values can't be transplanted.

Around the engines, the same facade offers a plain-message logger that scrubs stack traces and a per-thread NSLogContext for trace/span/request correlation — all Java 8+, all with zero third-party dependencies outside the log module's logback-core.

the whole API in one glance
// one facade, every operation
NS.use(NSOp.LOG, LOGIN_SCHEMA, event);        // masked log line
NS.use(NSOp.GENERATE, ORDER_CREATE, order);    // fill empty id fields
NS.use(NSOp.CRYPTO, CUSTOMER_WRITE, record);   // field-level encrypt
NS.use(NSOp.VERIFY, attempt, storedHash);      // constant-time check
String id = NS.use(NSOp.ID);                    // one ULID

// plain messages — scrubbed, stack traces included
NS.log(OrderController.class).info("Order {} shipped", id);

// uniqueness-checked generation, DB stays the authority
NS.use(NSOp.GENERATE, ORDER_CREATE, order,
       (field, v) -> repo.exists(field, v));
ns-secure-core

NS facade, NSOp, typed NSSchema (LOG / GENERATE / CRYPTO), NSLevel, NSLogContext, SPIs & registry, internal JSON. Zero deps.

ns-secure-log

Schema-driven masking engine + logback-core appenders (console, rolling file, TCP, Splunk HEC). Discovered via META-INF/services.

ns-secure-crypto

Field-level crypto: AES-GCM, ChaCha20-Poly1305, RSA-OAEP, HMAC-SHA256, PBKDF2, legacy AES-CBC. Key refs, not keys, in schemas.

ns-secure-id

ULID, UUIDv7, UUIDv4, SNOWFLAKE, RANDOM, NUMERIC, PATTERN — coordination-free, with uniqueness-check support.

The ns-secure umbrella artifact pulls all modules in one dependency — or depend on a single engine to keep the classpath minimal.

Functionality

What it actually does for you

Eight capabilities that turn "don't log PII" from a code-review hope into an enforced pipeline property.

Schema-driven events

Each event type is a versioned JSON schema: name, level, message and a per-path rule map. Validated and compiled at class-init — bad schemas fail fast.

Deny by default

A field with no rule is suppressed (unlisted policy: SUPPRESS, REDACT or ALLOW). Forgot to classify it? It never leaves the process.

Path rules, specificity-ranked

a.b, list[*].x, list[0], **.iban, metrics.**. The most specific path wins; ties resolve to the stronger strategy.

Pattern + content floor

EMAIL, CARD (Luhn-checked), SSN, PHONE, IP, DOB, CREDENTIAL rules plus REG_EXP: customs. Allowed text is still scrubbed for secrets.

Stable pseudonyms

PSEUDONYM_RULE emits keyed tokens — correlate a user's events without ever logging the identity. Secret via env:/sysprop: ref.

Level floor + policies

Events below logging.level are not written; AUDIT always is. Per-level policies can force a strategy or tighten unlisted at ERROR.

Thread-local context

NSLogContext.with("traceId", …) attaches correlation IDs to every line — allowlisted keys only, since context values are not masked.

Enterprise sinks

STDOUT by default; drop in ns-secure-logback.xml for console, rolling file, async, TCP and Splunk HEC appenders — logback-core only, no slf4j needed.

Field-level crypto

NS.use(NSOp.CRYPTO, …) encrypts, hashes or drops each field per its rule's algo. Ciphertexts are AEAD-bound to schema/field — they can't be transplanted.

Unique-id generation

NS.use(NSOp.GENERATE, …) fills only empty id fields — ULID, UUIDv7, SNOWFLAKE, patterned keys — with an optional uniqueness callback against your store.

Architecture flow

From NS.use() to a safe log line

Click any stage to see what happens inside it. Data only moves left-to-right through the pipeline — there is no path that skips masking.

1
Your code
NS.use(NSOp.LOG, schema, event)
2
NS facade → registry
ServiceLoader discovers NSLogService
3
Schema compile
paths + patterns validated at init
4
Masking engine
path match → strategy → content floor
5
Level floor → sink
JSON line → stdout / file / TCP / Splunk
Flow · NS.use(NSOp.GENERATE, schema, record)
1 · facade — schema kind must be GENERATE, else IllegalArgumentException
▼
2 · NSIdService — found via ServiceLoader
▼
3 · rule compile at schema load — unknown types, conflicting bits/length, unbounded REG_EXP and small keyspaces fail or warn before use
▼
4 · per field — missing / null / "" → mint (ULID monotonic even in the same ms); existing values untouched, input map never mutated
▼
5 · optional exists-check — (field, value) → your store; a collision retries with a fresh value, then gives up
Flow · NS.use(NSOp.CRYPTO, schema, record)
1 · facade — schema kind must be crypto (ENCRYPT / DECRYPT / SEAL / OPEN)
▼
2 · NSSchemaCryptoService — found via ServiceLoader
▼
3 · rule compile at schema load — unknown algo (with a "did you mean"), keyRef required where needed, legacy AES-CBC needs opt-in, PBKDF2 cost ≥ 100k
▼
4 · per field — keyRef resolved fresh each call, algo applied (fresh nonce per AES-GCM write), values bound to <schema>/<field> context; strict mode rejects fields with no rule
▼
5 · returned copy — envelopes self-describe (version · alg · keyId · nonce); one generic NSCryptoException for every failure, so errors never leak which part was wrong
Layer model
your app — NS.use(op, schema, data) / NS.log
▼
ns-secure-core — facade · typed NSSchema · SPIs · registry · JSON
▼
ns-secure-log — masking engine + logback appenders
ns-secure-crypto — field-level AEAD / HMAC / PBKDF2
ns-secure-id — ULID · UUIDv7 · SNOWFLAKE · patterns
▼
sinks — stdout · file · TCP · Splunk HEC
Extensible by SPI

Every capability is an interface in core: NSLogService, NSIdService, NSSchemaCryptoService, plus key resolution via NSKeys.register(resolver). Implement yours, register it in META-INF/services or via NSServiceRegistry.register(), and the facade serves it — ideal for HSM/KMS-backed crypto or test sinks.

The bundled engines are just SPI implementations themselves — there is no privileged path.

Configuration

One classpath file — /ns-secure.json — tunes the engine: service name, level floor, unlisted policy, content rules, per-level policies, context keys, mask character, pseudonym secret ref. Every key optional, every typo a startup error.

Environment-aware: ns.secure.env=prod makes the engine refuse scrubbing: OFF unless you explicitly allow it.

Rule vocabulary

Built-in rules

RuleStrategyMatches
CREDENTIAL_RULEREDACTtokens — ghp_, xox, sk, AKIA, JWT eyJ
CARD_RULELAST413–19 digit numbers, Luhn-validated
SSN_RULELAST4ddd-dd-dddd
EMAIL_RULEPARTIALRFC-shaped addresses
PHONE_RULELAST4international / local phone shapes
IP_RULEPRESERVE_FORMATIPv4 dotted quads
DOB_RULEREDACTyyyy-MM-dd
PSEUDONYM_RULEPSEUDONYMany value → stable keyed token
REDACT_RULEREDACTany value
ALLOW_RULENONEpasses — still scrubbed by content floor
ALLOW_RAWNONEpasses — exempt even from content floor
REG_EXP:<re>REDACTyour own pattern, inline in the schema

Pattern rules only apply when the value actually matches; a non-matching field falls through to the next rule or the unlisted policy.

Mask strategies

Ranked by strength

Hover a bar — strategies are ranked by strength. Ties between equally specific paths resolve to the stronger strategy.


Per-level policies can force a strategy globally — e.g. "DEBUG": {"forceStrategy": "REDACT"} turns even verbose debug output into fully redacted lines.

The engines

Two more engines behind the same facade

Logging is only the first NSOp. The same typed-schema pattern drives field-level cryptography and unique-id generation — pick the artifact, the classpath discovers the engine.

Crypto engine · ns-secure-crypto

Field-level encryption, schema-declared

The schema's top-level type sets the direction — ENCRYPT/DECRYPT for symmetric, SEAL/OPEN for public-key — and each rule's algo says what happens to that field. Keys live in keyRef references, never in code or schemas.

AlgoKindUse
AES-GCMreversibledefault AEAD field encryption
ChaCha20-Poly1305reversibleAEAD on Java 11+
RSA-OAEPreversibleSEAL/OPEN hybrid, per-rule keyRef
AES-CBCreversiblelegacy interop — requires "legacy": true
HMAC-SHA256irreversible"searchable": true → blind index
PBKDF2irreversiblepasswords; VERIFY re-derives from stored salt+cost
PLAIN / DROPnonepass through / remove the field
  • Context-bound ciphertext

    Every value is AEAD-bound to <schema>/<field> — a ciphertext copied into another field or schema fails to decrypt.

  • Key references, not keys

    env:NAME (Base64/PEM), file:/path, file:/ks.p12#alias — plus custom vault:/kms: schemes via NSKeys.register(). New keys with NSKeys.generate().

  • Constant-time verify

    NS.use(NSOp.VERIFY, attempt, stored) re-derives PBKDF2 with its own salt and cost; everything else compares in constant time.

customer-encrypt.json
{
  "name": "customer", "type": "ENCRYPT",
  "keyRef": "env:NS_SECURE_KEY", "strict": true,
  "rules": {
    "ssn":       { "algo": "AES-GCM" },
    "payload":   { "algo": "RSA-OAEP", "keyRef": "vault:payments-pub" },
    "email":     { "algo": "HMAC-SHA256", "searchable": true },
    "password":  { "algo": "PBKDF2" },
    "consentId": { "algo": "PLAIN" },
    "notes":     { "algo": "DROP" }
  }
}

// same rules, direction flips with the schema type
NS.use(NSOp.CRYPTO, CUSTOMER_W, data);  // ENCRYPT
NS.use(NSOp.CRYPTO, CUSTOMER_R, row);   // DECRYPT — tokens & hashes stay as-is
order-create.json
{
  "name": "order.create", "type": "GENERATE",
  "rules": {
    "orderId":       "ULID",
    "publicRef":    "UUID4",
    "correlationId": "UUID7",
    "otp":          "NUMERIC:6",
    "apiKey":       { "type": "RANDOM", "alphabet": "base62",
                      "bits": 256, "prefix": "sk_live_" },
    "licenseKey":   { "type": "PATTERN",
                      "regex": "[A-HJ-NP-Z2-9]{4}-[A-HJ-NP-Z2-9]{4}-[A-HJ-NP-Z2-9]{4}" }
  }
}

// fills only missing / null / "" fields — idempotent
NS.use(NSOp.GENERATE, ORDER_CREATE, order);
// regenerate on collision; the DB stays the authority
NS.use(NSOp.GENERATE, ORDER_CREATE, order,
       (field, v) -> repo.exists(field, v));
Id engine · ns-secure-id

Ids minted at the schema's word

A "type": "GENERATE" schema names which fields get ids and in what format. Nested paths and items[*].id arrays work; everything except SNOWFLAKE is coordination-free — any node mints alone, no shared state.

TypeShape
ULID · UUID7time-ordered, sort-friendly
UUID4random UUID
SNOWFLAKE64-bit, needs a unique nodeId per instance
RANDOMalphabet · bits/length · prefix · case · "groups": "4-4-4"
NUMERIC:ndigits, e.g. 6-digit OTP
PATTERNgenerates to a regex — license-key shapes

Presets for the common cases: API_KEY, PUBLIC_ID, SORT_KEY, HUMAN_CODE, OTP. Shorthands: RANDOM:<bytes>[:prefix], NUMERIC:<len>, REG_EXP:<regex>.

The engine warns at schema-compile time when a key-like field has under 64 bits of entropy — collision and guessing risk flagged before production, not after.

Live demos

Watch each engine at work

Three browser-side simulations, one per engine:

Demo 1 · log engine Demo 2 · id engine Demo 3 · crypto engine

A browser-side simulation of NSDefaultLogService — same rules, same deny-by-default. Edit the schema or the event, then run it. Notice that password has no rule — it never appears.

NS.use(NSOp.LOG, schema, event) — simulated
schema · auth-login.json
event payload
emitted line

Try it: remove a rule from the schema and watch the field disappear entirely; add "department": "ALLOW_RULE" to keep it; change requestBody to ALLOW_RAW to bypass the content floor (the card number comes through — exactly why it's opt-in).

Demo 2 · mint unique ids

A browser-side simulation of the id engine. Only empty fields (missing / null / "") get values — existing ids survive, so correlationId and lines[1].lineId stay as they are.

NS.use(NSOp.GENERATE, schema, order) — simulated
schema · order-create.json
record (empty fields get ids)
returned record

Try it: change apiKey's bits to 24 — the engine warns that the keyspace is too small for unattended generation; run twice and note ULIDs keep ordering.

Demo 3 · encrypt, tokenise, hash, drop

A browser-side simulation of the crypto engine (deterministic stand-ins, not real AES/PBKDF2). Note the AES-GCM envelope changes every run — fresh nonce — while HMAC tokens and the PBKDF2 hash are stable for the same input.

NS.use(NSOp.CRYPTO, schema, row) — simulated
schema · order-store.json (ENCRYPT)
row to store
stored row + read-back

Try it: cvv has no rule and strict is true — the strict error names the field but never its value. Remove it (or set strict:false) and note notes is dropped entirely. On DECRYPT the tokens and hash stay as-is — only reversible algos come back.

Runtime reconfiguration

Change policy without a restart

There is no file watcher: the application swaps in a new NSSchema or registers a new engine, and the next call uses it. Schemas and loggers already in flight pick it up immediately; a broken replacement is rejected while the previous good one keeps working — verified with 200 live swaps under 8-thread load: zero errors, zero unmasked data.

WhatLive?How
Log rules — paths, masking, level-scopedYesNSSchema.fromJson() → swap into an AtomicReference; bad JSON / unknown rule throws, old schema stays
Log config — level floor, service, mask char, message rulesYesNSServiceRegistry.register(NSLogService, new NSDefaultLogService(NSLogSettings.fromJson(json), sink))
Outputs & per-logger levels — ns-secure-logback.xmlYesNew engine with NSLogbackSink.fromXml()/fromUrl(), then close() the old one to flush files/sockets
Auto reload on file changeNoNot built in — trigger a reload yourself (admin endpoint, file timestamp check, Spring Cloud refresh, ConfigMap watch)
Id formats — length, alphabet, pattern, prefixYesSwap the GENERATE schema
Snowflake node idYesregister(new NSDefaultIdService(NSIdSettings.withNodeId(n))) — register before loading SNOWFLAKE schemas
Crypto policy — algo per field, keyRefYesSwap the ENCRYPT/DECRYPT schema pair (same name)
Keys in file / vaultYesResolved on every call — a key replaced in the store is used immediately
Keys in env varsNoA JVM can't change its own env — rotate env: keys with a restart, or use file:/vault: where live rotation matters
Data from the previous keyPartlyNo built-in keyring — old ciphertext fails with the generic error unless you keep versioned keyRefs (card-key-v1 read, card-key-v2 write)
RuntimeConfig.java — the pattern
// hold schemas in a swappable reference
private static final AtomicReference<NSSchema> ORDER =
    new AtomicReference<>(NSSchema.fromResource("/schemas/order.json"));

// POST /admin/schema reload — next call uses new rules
ORDER.set(NSSchema.fromJson(newJson));            // throws on a bad edit

// change engine settings live
NSServiceRegistry.register(NSLogService.class,
    new NSDefaultLogService(
        NSLogSettings.fromJson(newSettingsJson),  // level, maskChar, service
        NSLogbackSink.fromXml(newLogbackXml)));   // new outputs

// rotate a key in the secrets store — next call uses it
// (env: keys can't change inside a running JVM)

Typical production pattern: your ops UI or a scheduled job re-reads the schema/config source and calls set()/register() — the framework makes the swap atomic, you decide the trigger.

Get started

One dependency, one schema, one call

dependencies {
    // umbrella: facade + all three engines
    implementation 'io.github.nsprism:ns-secure:0.1.0-beta.1'

    // or just the engines you need:
    // 'io.github.nsprism:ns-secure-log:0.1.0-beta.1'
    // 'io.github.nsprism:ns-secure-crypto:0.1.0-beta.1'
    // 'io.github.nsprism:ns-secure-id:0.1.0-beta.1'
}
<dependency>
    <groupId>io.github.nsprism</groupId>
    <artifactId>ns-secure</artifactId>
    <version>0.1.0-beta.1</version>
</dependency>
{
  "logging": {
    "service": "claims-api",
    "level": "INFO",
    "unlisted": "SUPPRESS",
    "contentRules": ["CREDENTIAL_RULE", "CARD_RULE", "SSN_RULE"],
    "levelPolicy": {
      "DEBUG": { "forceStrategy": "REDACT" },
      "ERROR": { "unlisted": "REDACT" }
    }
  },
  "pseudonym": { "secretRef": "env:NS_SECURE_PEPPER" }
}
<!-- classpath root: console + rolling file + async TCP + Splunk HEC -->
<configuration>
  <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    <encoder class="io.github.nsprism.nssecure.log.logback.NSJsonEncoder"/>
  </appender>
  <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>logs/app.json</file>
    <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
      <fileNamePattern>logs/app.%d.%i.json.gz</fileNamePattern>
      <maxFileSize>10MB</maxFileSize><maxHistory>14</maxHistory>
    </rollingPolicy>
    <encoder class="io.github.nsprism.nssecure.log.logback.NSJsonEncoder"/>
  </appender>
  <appender name="SPLUNK" class="io.github.nsprism.nssecure.log.logback.NSSplunkHecAppender">
    <url>${SPLUNK_HEC_URL}</url><token>${SPLUNK_HEC_TOKEN}</token>
  </appender>
  <root level="INFO">
    <appender-ref ref="CONSOLE"/><appender-ref ref="FILE"/>
    <appender-ref ref="SPLUNK"/>
  </root>
</configuration>

No ns-secure.json? Defaults are already safe: INFO floor, SUPPRESS unlisted, credentials/cards/SSNs scrubbed, per-JVM pseudonym key.

three lines to a safe event
// 1 — declare the schema once (classpath resource)
private static final NSSchema LOGIN =
    NSSchema.fromResource("/ns-schemas/auth-login.json");

// 2 — build the event
Map<String, Object> event = new HashMap<>();
event.put("userId", principal.getId());
event.put("sessionToken", token);
event.put("user", user);   // nested maps & lists are walked

// 3 — emit; returns the masked payload actually written
NS.use(NSOp.LOG, LOGIN, event);
Why it matters

Built for enterprise gravity

The difference between a masking library and a security control is what happens when someone makes a mistake. ns-secure is designed around the mistakes.

  • Compliance by construction

    GDPR, PCI-DSS and HIPAA all care about PII in logs. Deny-by-default means an undeclared field — the classic leak vector — is physically absent from output. Auditors see a policy file, not a promise.

  • Audit events can't be silenced

    AUDIT level events bypass the level floor unconditionally — security-relevant records are written even when a service is tuned to ERROR.

  • Fail-fast misconfiguration

    Unknown config keys, bad rules, malformed paths and missing secrets all throw at class-initialisation or startup. You cannot deploy a silently-partial policy.

  • Production guardrails

    With ns.secure.env=prod, switching scrubbing off is refused unless allowUnscrubbedInProduction=true — an explicit, reviewable opt-out.

  • Supply-chain-friendly

    Core has zero third-party dependencies — even JSON parsing is internal. One small jar to vet, no transitive surprises in your SBOM.

  • Correlation without identity

    Pseudonymised IDs are stable per secret, so dashboards can still group a user's session — while the raw identity never leaves the service boundary.

  • Fits existing observability

    JSON lines to stdout work with any collector; the optional logback config adds async, rolling files, TCP and Splunk HEC without pulling slf4j or a logging framework.

  • Extends, doesn't fork

    HSM-backed crypto, KMS-resolved secrets, a custom log engine — all are SPI implementations. Register them on the classpath; the facade and every call site stay unchanged.

Add it to the classpath. The rest is policy.

Back to the framework family, or grab the coordinates and start masking.

io.github.nsprism:ns-secure:0.1.0-beta.1
All frameworks Source on GitHub