nc.crypto
Crypto without the footguns, built on node:crypto: hashes, HMAC, password hashing (scrypt), encryption (AES-256-GCM), JWT, two-factor codes (TOTP) and secure random values.
const { crypto } = require("@ix-xs/node-comfort");
import { hash } from "@ix-xs/node-comfort/crypto";const stored = await crypto.hashPassword("hunter2");
await crypto.verifyPassword("hunter2", stored); // true
const token = crypto.signJWT({ sub: user.id }, SECRET, { expiresIn: "1h" });
const claims = crypto.verifyJWT(token, SECRET);nc.crypto.Functions
nc.crypto.hash()
hash(data: string | Buffer<ArrayBufferLike> | Uint8Array<ArrayBufferLike>, options?: HashOptions): stringHashes data, SHA-256 in hex by default.
Parameters
| Name | Type | Description |
|---|---|---|
data | string | Buffer | Uint8Array | Strings are read as UTF-8. |
optionsoptional | HashOptions |
Returns string
Example
crypto.hash("hello");
crypto.hash("hello", { algorithm: "sha512", encoding: "base64url" });nc.crypto.hmac()
hmac(data: string | Buffer<ArrayBufferLike> | Uint8Array<ArrayBufferLike>, secret: string | Buffer<ArrayBufferLike> | Uint8Array<ArrayBufferLike>, options?: HashOptions): stringAn HMAC signature: a hash keyed with a secret. The usual way to sign and check webhooks.
Parameters
| Name | Type |
|---|---|
data | string | Buffer | Uint8Array |
secret | string | Buffer | Uint8Array |
optionsoptional | HashOptions |
Returns string
Example
const signature = crypto.hmac(rawBody, process.env.WEBHOOK_SECRET);
crypto.safeEqual(signature, req.headers["x-signature"]);nc.crypto.safeEqual()
safeEqual(a: string | Buffer<ArrayBufferLike> | Uint8Array<ArrayBufferLike>, b: string | Buffer<ArrayBufferLike> | Uint8Array<ArrayBufferLike>): booleanCompares two secrets in constant time, so the comparison can't leak anything through timing. Use it instead of === for tokens, signatures and API keys.
Parameters
| Name | Type |
|---|---|
a | string | Buffer | Uint8Array |
b | string | Buffer | Uint8Array |
Returns boolean
Example
if (!crypto.safeEqual(providedKey, process.env.API_KEY)) throw new Error("Forbidden");nc.crypto.hashPassword()
hashPassword(password: string, options?: HashPasswordOptions): Promise<string>Hashes a password with scrypt and a random salt. The result records its own settings, so you can raise the cost later without breaking old hashes.
Parameters
| Name | Type |
|---|---|
password | string |
optionsoptional | HashPasswordOptions |
Returns Promise<string>
Example
const stored = await crypto.hashPassword(password);
// "$scrypt$ln=15,r=8,p=1$4xY...$kq9...", store thisnc.crypto.verifyPassword()
verifyPassword(password: string, stored: string): Promise<boolean>Checks a password against a stored hash, in constant time. A wrong password or a broken hash gives false, never an error.
Parameters
| Name | Type |
|---|---|
password | string |
stored | string |
Returns Promise<boolean>
Example
if (!(await crypto.verifyPassword(input, user.passwordHash))) throw new Error("Invalid credentials");nc.crypto.needsRehash()
needsRehash(stored: string, options?: HashPasswordOptions): booleanWas this hash made with weaker settings than yours? If so, re-hash the password right after a successful login.
Parameters
| Name | Type | Description |
|---|---|---|
stored | string | |
optionsoptional | HashPasswordOptions | The settings you use today. |
Returns boolean
Example
if (await crypto.verifyPassword(input, user.hash) && crypto.needsRehash(user.hash, { cost: 16 })) {
user.hash = await crypto.hashPassword(input, { cost: 16 });
}nc.crypto.generateKey()
generateKey(): stringA random 256-bit key for encrypt(), as text you can put in an environment variable.
Returns string
nc.crypto.encrypt()
encrypt(data: string | Buffer<ArrayBufferLike> | Uint8Array<ArrayBufferLike>, secret: string | Buffer<ArrayBufferLike> | Uint8Array<ArrayBufferLike>, options?: EncryptOptions): stringEncrypts data with AES-256-GCM, which also detects tampering. Every call uses a fresh salt and nonce, so the same text never encrypts the same way twice.
Use a key from generateKey() or another long random secret, not a password someone typed: that would be easy to brute-force.
Parameters
| Name | Type |
|---|---|
data | string | Buffer | Uint8Array |
secret | string | Buffer | Uint8Array |
optionsoptional | EncryptOptions |
Returns string: A compact, URL-safe string.
Example
const box = crypto.encrypt(JSON.stringify(card), process.env.ENCRYPTION_KEY);
const card = JSON.parse(crypto.decrypt(box, process.env.ENCRYPTION_KEY));nc.crypto.decrypt()2 overloads
decrypt(payload: string, secret: string | Buffer<ArrayBufferLike> | Uint8Array<ArrayBufferLike>, options: DecryptOptions & { output: "buffer"; }): Buffer<ArrayBufferLike>
decrypt(payload: string, secret: string | Buffer<ArrayBufferLike> | Uint8Array<ArrayBufferLike>, options?: DecryptOptions): stringDecrypts what encrypt() produced. Throws if the secret is wrong or the data was changed.
Parameters
| Name | Type |
|---|---|
payload | string |
secret | string | Buffer | Uint8Array |
options | DecryptOptions & { output: "buffer" } |
Returns Buffer
Example
const text = crypto.decrypt(box, SECRET);
const bytes = crypto.decrypt(box, SECRET, { output: "buffer" });nc.crypto.signJWT()
signJWT(payload: Record<string, unknown>, secret: string | Buffer<ArrayBufferLike>, options?: SignJWTOptions): stringCreates a signed JSON Web Token. iat is added for you.
Parameters
| Name | Type | Description |
|---|---|---|
payload | Record<string, unknown> | |
secret | string | Buffer | At least 32 random bytes. |
optionsoptional | SignJWTOptions |
Returns string
Example
const token = crypto.signJWT({ sub: String(user.id), role: user.role }, process.env.JWT_SECRET, {
expiresIn: "15m",
issuer: "api.example.com",
});nc.crypto.decodeJWT()
decodeJWT<P = Record<string, unknown>>(token: string): DecodedJWT<P>Reads a JWT without checking it. Fine for display; call verifyJWT() before trusting anything in it.
Parameters
| Name | Type |
|---|---|
token | string |
Returns DecodedJWT<P>
Throws JWTError With code ERR_JWT_MALFORMED if the token can't be read.
Example
crypto.decodeJWT(token).payload.exp;nc.crypto.verifyJWT()
verifyJWT<P = Record<string, unknown>>(token: string, secret: string | Buffer<ArrayBufferLike>, options?: VerifyJWTOptions): P & JWTClaimsVerifies a JWT: algorithm, signature, expiry, and whichever claims you require. Returns the payload, or throws a JWTError whose code says what's wrong.
Parameters
| Name | Type |
|---|---|
token | string |
secret | string | Buffer |
optionsoptional | VerifyJWTOptions |
Returns P & JWTClaims
Throws JWTError
Example
try {
const { sub } = crypto.verifyJWT(token, process.env.JWT_SECRET, { issuer: "api.example.com" });
} catch (error) {
res.status(401).json({ error: error.code }); // "ERR_JWT_EXPIRED"...
}nc.crypto.totpSecret()
totpSecret(bytes?: number): stringA new two-factor secret, in the Base32 format authenticator apps expect. Store it encrypted.
Parameters
| Name | Type | Description |
|---|---|---|
bytesoptional | number | Default: 20 |
Returns string
Example
user.totpSecret = crypto.totpSecret();nc.crypto.totp()
totp(secret: string, options?: TOTPOptions): stringThe current 6-digit code for a two-factor secret, as shown by authenticator apps.
Parameters
| Name | Type |
|---|---|
secret | string |
optionsoptional | TOTPOptions |
Returns string
Example
crypto.totp(user.totpSecret); // "492039"nc.crypto.verifyTOTP()
verifyTOTP(token: string, secret: string, options?: VerifyTOTPOptions): booleanChecks a code typed by a user. Codes from the neighbouring periods are accepted too, to cope with clock drift.
Parameters
| Name | Type | Description |
|---|---|---|
token | string | Spaces are ignored. |
secret | string | |
optionsoptional | VerifyTOTPOptions |
Returns boolean
Example
if (!crypto.verifyTOTP(req.body.code, user.totpSecret)) throw new Error("Invalid code");nc.crypto.totpURI()
totpURI(secret: string, options: TOTPURIOptions): stringThe otpauth:// link to show as a QR code when a user turns on 2FA.
Parameters
| Name | Type |
|---|---|
secret | string |
options | TOTPURIOptions |
Returns string
Example
const uri = crypto.totpURI(user.totpSecret, { label: user.email, issuer: "My App" });nc.crypto.randomBytes()
randomBytes(size: number): Buffer<ArrayBufferLike>Secure random bytes.
Parameters
| Name | Type |
|---|---|
size | number |
Returns Buffer
nc.crypto.randomInt()
randomInt(min: number, max: number): numberA secure random integer from min to max, both included, with no bias. For anything that has to be fair or unpredictable.
Parameters
| Name | Type |
|---|---|
min | number |
max | number |
Returns number
Example
crypto.randomInt(1, 6); // a fair dice roll
crypto.randomInt(0, 999_999); // a 6-digit codenc.crypto.toBase64()
toBase64(data: string | Buffer<ArrayBufferLike> | Uint8Array<ArrayBufferLike>, options?: { urlSafe?: boolean; } | undefined): stringEncodes to Base64, or URL-safe Base64.
Parameters
| Name | Type |
|---|---|
data | string | Buffer | Uint8Array |
optionsoptional | { urlSafe?: boolean } |
Returns string
Example
crypto.toBase64("héllo"); // "aMOpbGxv"
crypto.toBase64("héllo", { urlSafe: true });nc.crypto.fromBase64()2 overloads
fromBase64(text: string, options: { output: "buffer"; }): Buffer<ArrayBufferLike>
fromBase64(text: string, options?: { output?: "utf8"; } | undefined): stringDecodes Base64 or URL-safe Base64.
Parameters
| Name | Type |
|---|---|
text | string |
options | { output: "buffer" } |
Returns Buffer
Example
crypto.fromBase64("aMOpbGxv"); // "héllo"
crypto.fromBase64("aMOpbGxv", { output: "buffer" }); // <Buffer 68 c3 a9 6c 6c 6f>Types
Import any of them in TypeScript with import type { DecodedJWT } from "@ix-xs/node-comfort", or in JavaScript with import("@ix-xs/node-comfort").DecodedJWT.
DecodedJWT
A decoded JWT.
| Property | Type | Description |
|---|---|---|
header | { [key: string]: unknown; alg: string; typ?: string; } | |
payload | P & JWTClaims | |
signature | string | Base64URL. |
DecryptOptions
Options for decrypt().
| Property | Type | Description |
|---|---|---|
associatedDataoptional | string | Buffer<ArrayBufferLike> | The value given to encrypt(), if any. |
outputoptional | "buffer" | "utf8" | Get a string or a Buffer. Defaults to "utf8". |
DigestEncoding
Text encodings for digests.
type DigestEncoding = "hex" | "base64" | "base64url"EncryptOptions
Options for encrypt().
| Property | Type | Description |
|---|---|---|
associatedDataoptional | string | Buffer<ArrayBufferLike> | Data that's authenticated but not encrypted, like a user id. Pass the same value to decrypt(). |
HashOptions
Options for hash() and hmac().
| Property | Type | Description |
|---|---|---|
algorithmoptional | (string & {}) | "sha256" | "sha384" | "sha512" | "sha1" | "md5" | "sha3-256" | "sha3-512" | Any algorithm OpenSSL supports. Defaults to "sha256". |
encodingoptional | DigestEncoding | Defaults to "hex". |
HashPasswordOptions
Options for hashPassword().
| Property | Type | Description |
|---|---|---|
costoptional | number | Work factor as a power of two; each +1 doubles the time. Defaults to 15. |
blockSizeoptional | number | scrypt r. Defaults to 8. |
parallelizationoptional | number | scrypt p. Defaults to 1. |
saltLengthoptional | number | In bytes. Defaults to 16. |
keyLengthoptional | number | In bytes. Defaults to 32. |
JWTAlgorithm
Supported JWT algorithms.
type JWTAlgorithm = "HS256" | "HS384" | "HS512"JWTClaims
Standard JWT claims.
| Property | Type | Description |
|---|---|---|
issoptional | string | Issuer. |
suboptional | string | Subject, usually the user id. |
audoptional | string | string[] | Audience. |
expoptional | number | Expiry, in Unix seconds. |
nbfoptional | number | Not valid before, in Unix seconds. |
iatoptional | number | Issued at, in Unix seconds. |
jtioptional | string | Unique token id. |
SignJWTOptions
Options for signJWT().
| Property | Type | Description |
|---|---|---|
algorithmoptional | JWTAlgorithm | Defaults to "HS256". |
expiresInoptional | string | number | Lifetime in seconds or as a duration like "15m". Sets exp. |
notBeforeoptional | string | number | Delay before the token is valid. Sets nbf. |
issueroptional | string | Sets iss. |
subjectoptional | string | Sets sub. |
audienceoptional | string | string[] | Sets aud. |
jwtIdoptional | string | Sets jti, handy for revocation lists. |
noTimestampoptional | boolean | Don't add iat. |
headeroptional | Record<string, unknown> | Extra header fields, like { kid: "2024-key" }. |
TOTPOptions
Options for totp() and verifyTOTP().
| Property | Type | Description |
|---|---|---|
digitsoptional | number | Defaults to 6. |
periodoptional | number | How long a code lasts, in seconds. Defaults to 30. |
algorithmoptional | "SHA1" | "SHA256" | "SHA512" | Defaults to "SHA1", which is what authenticator apps expect. |
timeoptional | number | Date | Compute the code for this moment. Defaults to now. |
TOTPURIOptions
Options for totpURI().
| Property | Type | Description |
|---|---|---|
label | string | The account shown in the app, usually the email. |
issueroptional | string | Your app's name. |
digitsoptional | number | Defaults to 6. |
periodoptional | number | Defaults to 30. |
algorithmoptional | "SHA1" | "SHA256" | "SHA512" | Defaults to "SHA1". |
VerifyJWTOptions
Options for verifyJWT().
| Property | Type | Description |
|---|---|---|
algorithmsoptional | JWTAlgorithm[] | Accepted algorithms. Defaults to all three HMAC ones. |
issueroptional | string | string[] | Required issuer. |
audienceoptional | string | string[] | Required audience; one match is enough. |
subjectoptional | string | Required subject. |
clockToleranceoptional | number | Seconds of slack for exp and nbf when server clocks differ. |
maxAgeoptional | string | number | Reject tokens issued longer ago than this. |
ignoreExpirationoptional | boolean | Skip the exp check. |
nowoptional | number | Date | What time it is. Defaults to now. |
VerifyTOTPOptions
Options for verifyTOTP(). window is how many periods before and after now are accepted (defaults to 1).
type VerifyTOTPOptions = TOTPOptions & { window?: number; }