Skip to main content
jg-packages

Search

    Crypto

    jg-crypto

    Hashing, random bytes and digits, and HMAC signatures with constant-time verification, for Node.js.

    v0.1.1Node.jsESMMIT

    npm install jg-crypto

    Usage

    Generate an opaque token, such as for a sign-in link, and store only its hash, so a leaked table can’t be used to sign in:

    ts
    import { generateRandomBytes, hashValue } from 'jg-crypto';
    
    const token = generateRandomBytes(32); // 43 base64url characters, sent to the user
    const tokenHash = hashValue(token); // 64 hex characters, stored in the database

    Generate a one-time code for an email, with its leading zeros kept:

    ts
    import { generateRandomDigits } from 'jg-crypto';
    
    const code = generateRandomDigits(6); // such as '042917'

    Verify a webhook signature over the request’s raw body, compared in constant time:

    ts
    import { verifyHmac } from 'jg-crypto';
    
    const isAuthentic = verifyHmac(rawBody, signature, webhookSecret);

    Give each purpose its own signing key with a domain, so a signature made for one kind of value never verifies for another, even under the same secret:

    ts
    import { signHmac, verifyHmac } from 'jg-crypto';
    
    const signature = signHmac(cursorJson, secret, { domain: 'cursor' });
    verifyHmac(cursorJson, signature, secret, { domain: 'checkpoint' }); // false

    verifyHmac returns false for any signature but the exact one, and never throws for a bad signature. An empty secret or domain throws, because it’s the program’s own mistake.

    Generated from TSDoc

    API reference

    Every export of [email protected], 9 in all, with its signature, its errors, and an example.

    Functions

    generateRandomBytes

    function

    Generates cryptographically secure random bytes, encoded as text, such as an opaque token for a link or a session. 32 bytes give 256 bits: 43 base64url characters or 64 hex characters.

    Signature
    generateRandomBytes(byteLength: number, encoding?: IRandomBytesEncoding): string

    Parameters

    byteLengthnumber

    How many random bytes to generate, an integer from 1 to 65536.

    encoding?IRandomBytesEncoding

    How to encode them.

    Defaults to 'base64url'.

    Returns

    string

    The encoded bytes.

    Throws

    • INVALID_BYTE_LENGTHThe byte length must be an integer from 1 to 65536.
    • UNSUPPORTED_ENCODINGThe encoding must be base64url or hex.
    Example
    import { generateRandomBytes } from 'jg-crypto';
    
    generateRandomBytes(32); // 43 base64url characters, such as 'q3Rz…'
    generateRandomBytes(16, 'hex'); // 32 hex characters

    generateRandomDigits

    function

    Generates a string of cryptographically secure random digits, such as a one-time code to send by email. Every digit is drawn uniformly from 0 to 9, so leading zeros are as likely as any other digit and are kept.

    Signature
    generateRandomDigits(count: number): string

    Parameters

    countnumber

    How many digits to generate, an integer from 1 to 256.

    Returns

    string

    The digits.

    Throws

    • INVALID_DIGIT_COUNTThe digit count must be an integer from 1 to 256.
    Example
    import { generateRandomDigits } from 'jg-crypto';
    
    generateRandomDigits(6); // such as '042917'

    hashValue

    function

    Hashes a value with SHA-256, or another SHA-2 algorithm, and returns the lowercase hex digest. A string is hashed as its UTF-8 bytes, so a string and its encoded bytes give the same digest.

    Use it to store a token's hash instead of the token, or to compare values without keeping them. It's a plain digest, not a password hash: don't use it for passwords, which need a slow, salted function such as scrypt.

    Signature
    hashValue(
      value: string | Uint8Array<ArrayBufferLike>,
      algorithm?: IHashAlgorithm,
    ): string

    Parameters

    valuestring | Uint8Array<ArrayBufferLike>

    The string or bytes to hash.

    algorithm?IHashAlgorithm

    The SHA-2 algorithm.

    Defaults to 'sha256'.

    Returns

    string

    The digest, in lowercase hex: 64 characters for SHA-256.

    Throws

    • UNSUPPORTED_HASH_ALGORITHMThe hash algorithm must be sha256, sha384, or sha512.
    Example
    import { hashValue } from 'jg-crypto';
    
    hashValue('abc'); // 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad'
    hashValue('abc', 'sha512'); // a 128-character digest

    signHmac

    function

    Signs a message with HMAC-SHA256 and returns the signature in lowercase hex, 64 characters.

    Without a domain, it's the plain HMAC webhook providers compute, such as GitHub's X-Hub-Signature-256 without its sha256= prefix. With one, the key is derived from the secret and the domain, so the same secret can sign several kinds of values without a signature for one kind ever verifying for another.

    Signature
    signHmac(
      message: string | Uint8Array<ArrayBufferLike>,
      secret: string | Uint8Array<ArrayBufferLike>,
      options?: IHmacOptions,
    ): string

    Parameters

    messagestring | Uint8Array<ArrayBufferLike>

    The string or bytes to sign. A string is signed as its UTF-8 bytes, so sign a request's raw body as bytes rather than as text it was decoded to.

    secretstring | Uint8Array<ArrayBufferLike>

    The secret, a non-empty string or byte array.

    options?IHmacOptions

    The domain the signature is for, if any.

    Defaults to {}.

    Returns

    string

    The signature, in lowercase hex.

    Throws

    • INVALID_HMAC_SECRETThe HMAC secret must be a non-empty string or byte array.
    • INVALID_HMAC_DOMAINThe HMAC domain must be a non-empty string when it's given.
    Example
    import { signHmac } from 'jg-crypto';
    
    const signature = signHmac('{"page":2}', secret, { domain: 'cursor' }); // 64 hex characters

    verifyHmac

    function

    Checks a signature from signHmac, or from a webhook provider, comparing it in constant time so the time taken reveals nothing about how much of it matched.

    It returns false for anything but the exact signature: a different message, secret, or domain, an uppercase or truncated signature, or one that isn't hex. It never throws for a bad signature, because signatures come from outside the program, only for an invalid secret or domain, which are the program's own.

    Signature
    verifyHmac(
      message: string | Uint8Array<ArrayBufferLike>,
      signature: string,
      secret: string | Uint8Array<ArrayBufferLike>,
      options?: IHmacOptions,
    ): boolean

    Parameters

    messagestring | Uint8Array<ArrayBufferLike>

    The string or bytes that were signed.

    signaturestring

    The signature to check, 64 lowercase hex characters.

    secretstring | Uint8Array<ArrayBufferLike>

    The secret, a non-empty string or byte array.

    options?IHmacOptions

    The domain the signature is for, if any.

    Defaults to {}.

    Returns

    boolean

    Whether the signature is valid for the message.

    Throws

    • INVALID_HMAC_SECRETThe HMAC secret must be a non-empty string or byte array.
    • INVALID_HMAC_DOMAINThe HMAC domain must be a non-empty string when it's given.
    Example
    import { verifyHmac } from 'jg-crypto';
    
    const isAuthentic = verifyHmac(rawBody, signatureHeader.replace(/^sha256=/, ''), webhookSecret);

    Constants

    ERRORS

    const

    the codes for every error thrown by the package

    Declaration
    const ERRORS: { UNSUPPORTED_HASH_ALGORITHM: "UNSUPPORTED_HASH_ALGORITHM"; INVALID_BYTE_LENGTH: "INVALID_BYTE_LENGTH"; UNSUPPORTED_ENCODING: "UNSUPPORTED_ENCODING"; INVALID_DIGIT_COUNT: "INVALID_DIGIT_COUNT"; INVALID_HMAC_SECRET: "INVALID_HMAC_SECRET"; INVALID_HMAC_DOMAIN: "INVALID_HMAC_DOMAIN" }

    Types

    IHashAlgorithm

    type

    the SHA-2 algorithms hashValue computes

    Declaration
    type IHashAlgorithm = 'sha256' | 'sha384' | 'sha512';

    IHmacOptions

    type

    the options accepted by signHmac and verifyHmac

    Declaration
    type IHmacOptions = {
        domain?: string | undefined;
    };

    Members

    • domainproperty
      string

      The purpose the signature is for, such as 'cursor' or 'checkpoint'. The signing key becomes HMAC-SHA256(secret, domain), so a signature made for one purpose never verifies for another, even with the same secret. Without it, the secret signs directly, as webhook providers expect.

    IRandomBytesEncoding

    type

    the text encodings generateRandomBytes returns, both safe in URLs and file names

    Declaration
    type IRandomBytesEncoding = 'base64url' | 'hex';