node-comfortv2.0.0

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);
GuideExplanations and examples for nc.crypto.
Read the guide →

Functions

nc.crypto.hash()

hash(data: string | Buffer<ArrayBufferLike> | Uint8Array<ArrayBufferLike>, options?: HashOptions): string

Hashes data, SHA-256 in hex by default.

Parameters

NameTypeDescription
datastring | Buffer | Uint8ArrayStrings are read as UTF-8.
optionsoptionalHashOptions

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): string

An HMAC signature: a hash keyed with a secret. The usual way to sign and check webhooks.

Parameters

NameType
datastring | Buffer | Uint8Array
secretstring | Buffer | Uint8Array
optionsoptionalHashOptions

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>): boolean

Compares 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

NameType
astring | Buffer | Uint8Array
bstring | 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

NameType
passwordstring
optionsoptionalHashPasswordOptions

Returns Promise<string>

Example

const stored = await crypto.hashPassword(password);
// "$scrypt$ln=15,r=8,p=1$4xY...$kq9...", store this

nc.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

NameType
passwordstring
storedstring

Returns Promise<boolean>

Example

if (!(await crypto.verifyPassword(input, user.passwordHash))) throw new Error("Invalid credentials");

nc.crypto.needsRehash()

needsRehash(stored: string, options?: HashPasswordOptions): boolean

Was this hash made with weaker settings than yours? If so, re-hash the password right after a successful login.

Parameters

NameTypeDescription
storedstring
optionsoptionalHashPasswordOptionsThe 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(): string

A 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): string

Encrypts 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

NameType
datastring | Buffer | Uint8Array
secretstring | Buffer | Uint8Array
optionsoptionalEncryptOptions

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): string

Decrypts what encrypt() produced. Throws if the secret is wrong or the data was changed.

Parameters

NameType
payloadstring
secretstring | Buffer | Uint8Array
optionsDecryptOptions & { 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): string

Creates a signed JSON Web Token. iat is added for you.

Parameters

NameTypeDescription
payloadRecord<string, unknown>
secretstring | BufferAt least 32 random bytes.
optionsoptionalSignJWTOptions

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

NameType
tokenstring

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 & JWTClaims

Verifies a JWT: algorithm, signature, expiry, and whichever claims you require. Returns the payload, or throws a JWTError whose code says what's wrong.

Parameters

NameType
tokenstring
secretstring | Buffer
optionsoptionalVerifyJWTOptions

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): string

A new two-factor secret, in the Base32 format authenticator apps expect. Store it encrypted.

Parameters

NameTypeDescription
bytesoptionalnumberDefault: 20

Returns string

Example

user.totpSecret = crypto.totpSecret();

nc.crypto.totp()

totp(secret: string, options?: TOTPOptions): string

The current 6-digit code for a two-factor secret, as shown by authenticator apps.

Parameters

NameType
secretstring
optionsoptionalTOTPOptions

Returns string

Example

crypto.totp(user.totpSecret); // "492039"

nc.crypto.verifyTOTP()

verifyTOTP(token: string, secret: string, options?: VerifyTOTPOptions): boolean

Checks a code typed by a user. Codes from the neighbouring periods are accepted too, to cope with clock drift.

Parameters

NameTypeDescription
tokenstringSpaces are ignored.
secretstring
optionsoptionalVerifyTOTPOptions

Returns boolean

Example

if (!crypto.verifyTOTP(req.body.code, user.totpSecret)) throw new Error("Invalid code");

nc.crypto.totpURI()

totpURI(secret: string, options: TOTPURIOptions): string

The otpauth:// link to show as a QR code when a user turns on 2FA.

Parameters

NameType
secretstring
optionsTOTPURIOptions

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

NameType
sizenumber

Returns Buffer

nc.crypto.randomInt()

randomInt(min: number, max: number): number

A secure random integer from min to max, both included, with no bias. For anything that has to be fair or unpredictable.

Parameters

NameType
minnumber
maxnumber

Returns number

Example

crypto.randomInt(1, 6);       // a fair dice roll
crypto.randomInt(0, 999_999); // a 6-digit code

nc.crypto.toBase64()

toBase64(data: string | Buffer<ArrayBufferLike> | Uint8Array<ArrayBufferLike>, options?: { urlSafe?: boolean; } | undefined): string

Encodes to Base64, or URL-safe Base64.

Parameters

NameType
datastring | 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): string

Decodes Base64 or URL-safe Base64.

Parameters

NameType
textstring
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.

PropertyTypeDescription
header{ [key: string]: unknown; alg: string; typ?: string; }
payloadP & JWTClaims
signaturestringBase64URL.

DecryptOptions

Options for decrypt().

PropertyTypeDescription
associatedDataoptionalstring | 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().

PropertyTypeDescription
associatedDataoptionalstring | 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().

PropertyTypeDescription
algorithmoptional(string & {}) | "sha256" | "sha384" | "sha512" | "sha1" | "md5" | "sha3-256" | "sha3-512"Any algorithm OpenSSL supports. Defaults to "sha256".
encodingoptionalDigestEncodingDefaults to "hex".

HashPasswordOptions

Options for hashPassword().

PropertyTypeDescription
costoptionalnumberWork factor as a power of two; each +1 doubles the time. Defaults to 15.
blockSizeoptionalnumberscrypt r. Defaults to 8.
parallelizationoptionalnumberscrypt p. Defaults to 1.
saltLengthoptionalnumberIn bytes. Defaults to 16.
keyLengthoptionalnumberIn bytes. Defaults to 32.

JWTAlgorithm

Supported JWT algorithms.

type JWTAlgorithm = "HS256" | "HS384" | "HS512"

JWTClaims

Standard JWT claims.

PropertyTypeDescription
issoptionalstringIssuer.
suboptionalstringSubject, usually the user id.
audoptionalstring | string[]Audience.
expoptionalnumberExpiry, in Unix seconds.
nbfoptionalnumberNot valid before, in Unix seconds.
iatoptionalnumberIssued at, in Unix seconds.
jtioptionalstringUnique token id.

SignJWTOptions

Options for signJWT().

PropertyTypeDescription
algorithmoptionalJWTAlgorithmDefaults to "HS256".
expiresInoptionalstring | numberLifetime in seconds or as a duration like "15m". Sets exp.
notBeforeoptionalstring | numberDelay before the token is valid. Sets nbf.
issueroptionalstringSets iss.
subjectoptionalstringSets sub.
audienceoptionalstring | string[]Sets aud.
jwtIdoptionalstringSets jti, handy for revocation lists.
noTimestampoptionalbooleanDon't add iat.
headeroptionalRecord<string, unknown>Extra header fields, like { kid: "2024-key" }.

TOTPOptions

Options for totp() and verifyTOTP().

PropertyTypeDescription
digitsoptionalnumberDefaults to 6.
periodoptionalnumberHow long a code lasts, in seconds. Defaults to 30.
algorithmoptional"SHA1" | "SHA256" | "SHA512"Defaults to "SHA1", which is what authenticator apps expect.
timeoptionalnumber | DateCompute the code for this moment. Defaults to now.

TOTPURIOptions

Options for totpURI().

PropertyTypeDescription
labelstringThe account shown in the app, usually the email.
issueroptionalstringYour app's name.
digitsoptionalnumberDefaults to 6.
periodoptionalnumberDefaults to 30.
algorithmoptional"SHA1" | "SHA256" | "SHA512"Defaults to "SHA1".

VerifyJWTOptions

Options for verifyJWT().

PropertyTypeDescription
algorithmsoptionalJWTAlgorithm[]Accepted algorithms. Defaults to all three HMAC ones.
issueroptionalstring | string[]Required issuer.
audienceoptionalstring | string[]Required audience; one match is enough.
subjectoptionalstringRequired subject.
clockToleranceoptionalnumberSeconds of slack for exp and nbf when server clocks differ.
maxAgeoptionalstring | numberReject tokens issued longer ago than this.
ignoreExpirationoptionalbooleanSkip the exp check.
nowoptionalnumber | DateWhat 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; }
node-comfort v2.0.0View the source