@neotales/secrets
v0.0.0-alpha.0
Published
Cryptographically secure secret generation and log masking.
Maintainers
Readme
@neotales/secrets
Overview
@neotales/secrets generates cryptographically secure passwords and masks
registered sensitive values from strings, logs, and other text output. It works
in Deno, Node.js, Bun, and browsers that provide Web Crypto.
Installation
# Deno
deno add jsr:@neotales/secrets
# npm
npm install @neotales/secretsGenerate Secrets
generateSecret and secretGenerator use a cryptographically secure source
and require an uppercase letter, lowercase letter, digit, and special character.
import { DefaultSecretGenerator, generateSecret } from "@neotales/secrets";
const password = generateSecret(20);
const databasePassword = new DefaultSecretGenerator()
.addLower()
.addUpper()
.addDigits()
.add("_-#@")
.generate(32);generateAsUint8Array returns a mutable buffer for applications that can clear
the secret after use. Character pools must contain single-byte characters so
the string and byte APIs have the same length and value.
import { DefaultSecretGenerator } from "@neotales/secrets";
const secret = new DefaultSecretGenerator().addDefaults().generateAsUint8Array(24);
try {
// Use the secret.
} finally {
secret.fill(0);
}Use setValidator when the default password-policy validator does not fit your
use case. The validator receives the generated byte buffer and must return true
before a secret is returned.
Protect In-Memory Values
Secret encrypts UTF-8 text or bytes with AES-256-GCM and masks itself as
******* when converted to a string, serialized as JSON, or inspected in Node.
It is designed to make sensitive values explicit and prevent accidental logging.
import { Secret } from "@neotales/secrets";
const token = await Secret.fromText(process.env.API_TOKEN!);
console.log(token); // *******
console.log(JSON.stringify({ token })); // {"token":"*******"}
await token.withText(async (value) => {
await sendAuthenticatedRequest(value);
});
token.destroy();For the default process-local key, createSecret is the shorter factory for
either text or bytes. It intentionally accepts no key or IV options.
import { createSecret } from "@neotales/secrets";
const token = await createSecret("api-token");
const binary = await createSecret(new Uint8Array([1, 2, 3]));The root package is portable and asynchronous because it uses the Web Crypto
API, which is asynchronous and requires a secure browser context. Node.js,
Deno, and Bun can use the synchronous ./node subpath, which uses
node:crypto and AES-256-GCM.
import { Secret } from "@neotales/secrets/node";
const token = Secret.fromText("api-token");
request({ authorization: token.unprotectText() });The default key is generated randomly on first use, retained only for the current process, and cannot be exported. For values that must be decrypted by a different process or after restart, create and manage an explicit key yourself.
import { Secret, SecretKey } from "@neotales/secrets";
const key = SecretKey.generate();
const secret = await Secret.fromText("api-token", { key });
const rawKey = key.exportRaw();
// Store rawKey in a KMS, OS keychain, or another appropriate secret manager.
rawKey.fill(0);SecretKey.importRaw restores a previously stored 32-byte key. Exported raw
keys are as sensitive as all values they protect and must never be logged.
Encrypt Streams
encryptStream and decryptStream use framed AES-256-GCM. Each frame is at
most 64 KiB, uses a unique nonce, and is authenticated. A final authenticated
frame detects truncation. The transforms work with browser ReadableStreams as
well as runtime web streams.
import { decryptStream, encryptStream, SecretKey } from "@neotales/secrets";
const key = SecretKey.generate();
await source.pipeThrough(encryptStream({ key })).pipeTo(destination);
await encryptedSource.pipeThrough(decryptStream({ key })).pipeTo(plainDestination);For synchronous chunk processing in Node.js, Deno, and Bun, use
encryptChunks and decryptChunks from @neotales/secrets/node.
Cloudflare Workers
The root @neotales/secrets package works in Cloudflare Workers through the
standard asynchronous Web Crypto and Web Streams APIs. Do not import
@neotales/secrets/node in a Worker: that subpath is for the synchronous
node:crypto APIs available in Node.js, Deno, and Bun.
The default key is retained only in the current Worker isolate. Cloudflare can
evict an isolate at any time and can handle later requests in a different
isolate, so data protected with the default key must not be expected to decrypt
across requests, deployments, isolates, or restarts. A newly generated
SecretKey has the same limitation.
For data that must be decrypted after an isolate changes, import an explicit
32-byte key from a Workers secret, a KMS, or another durable key-management
system with SecretKey.importRaw. Store the key material separately from the
ciphertext and never include it in a response, log, or client bundle.
Security Limitations
This module is not a secure-memory implementation or a substitute for key management. It reduces accidental disclosure, but does not protect against a compromised process, debugger, malicious same-origin browser script, or a memory dump that includes the process key.
- JavaScript provides no portable way to pin memory or guarantee zeroization.
destroy()overwrites this module's writable buffers, but runtimes and native crypto implementations may create copies that cannot be cleared by JavaScript.fromText()receives an immutable caller-owned string, andunprotectText()creates another immutable string. Neither can be cleared.withBytes()clears the temporary byte buffer it creates after the callback, but callers can retain or copy that buffer.- An explicit
SecretKeybecomes unusable afterdestroy(), including for secrets and streams that reference it. - The process-local default key is intentionally not persistent. Restarting the process makes values protected with it unrecoverable.
Mask Secrets
Use an isolated DefaultSecretMasker when registrations must not be shared, or
use secretMasker for process-wide logging integrations. Plain strings are
trimmed, blank values are ignored, and every occurrence is masked. Regular
expressions are made global automatically.
import { DefaultSecretMasker } from "@neotales/secrets";
const masker = new DefaultSecretMasker()
.add("api-token-123")
.add(/password=\w+/i)
.addGenerator((secret) => secret.toUpperCase());
masker.mask("token=api-token-123 password=hunter2");
// "token=******* *******"addGenerator registers transformed forms of string secrets, such as encoded
or case-normalized forms. It applies to both existing and subsequently added
secrets.
API
| Export | Description |
| ------------------------------------- | ----------------------------------------------------- |
| generateSecret(length, characters?) | Generate a default-policy secret. |
| secretGenerator | Default configured DefaultSecretGenerator. |
| DefaultSecretGenerator | Configurable secure secret generator. |
| validate(bytes) | Default upper/lower/digit/special password validator. |
| secretMasker | Process-wide DefaultSecretMasker. |
| DefaultSecretMasker | String and regular-expression secret masker. |
| Secret | Encrypted in-memory, log-masked value. |
| SecretKey | Caller-managed AES-256-GCM key. |
| encryptStream / decryptStream | Portable framed AES-256-GCM stream transforms. |
