# jg-crypto

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

- **Version:** `0.1.1`
- **Runtimes:** Node.js
- **Install:** `npm install jg-crypto`
- **On npm:** [jg-crypto](https://www.npmjs.com/package/jg-crypto)
- **Source:** [packages/jg-crypto](https://github.com/jesusgraterol/jg-packages/tree/main/packages/jg-crypto)
- **Recipes:** [jg-crypto recipes](https://jg-packages.jesusgraterol.dev/packages/jg-crypto/recipes/index.md)

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

## <a id="api-reference"></a>jg-crypto API reference

Every export of `jg-crypto@0.1.1`, 9 in all, with its signature, its errors, and an example. It's generated from the package's TSDoc.

- Functions: [`generateRandomBytes`](#api-generateRandomBytes), [`generateRandomDigits`](#api-generateRandomDigits), [`hashValue`](#api-hashValue), [`signHmac`](#api-signHmac), [`verifyHmac`](#api-verifyHmac)
- Constants: [`ERRORS`](#api-ERRORS)
- Types: [`IHashAlgorithm`](#api-IHashAlgorithm), [`IHmacOptions`](#api-IHmacOptions), [`IRandomBytesEncoding`](#api-IRandomBytesEncoding)

### <a id="api-functions"></a>Functions

#### <a id="api-generateRandomBytes"></a>`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.

```ts
generateRandomBytes(byteLength: number, encoding?: IRandomBytesEncoding): string
```

**Parameters**

- `byteLength` `number`: 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_LENGTH`: The byte length must be an integer from 1 to 65536.
- `UNSUPPORTED_ENCODING`: The encoding must be base64url or hex.

**Example**

```ts
import { generateRandomBytes } from 'jg-crypto';

generateRandomBytes(32); // 43 base64url characters, such as 'q3Rz…'
generateRandomBytes(16, 'hex'); // 32 hex characters
```

#### <a id="api-generateRandomDigits"></a>`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.

```ts
generateRandomDigits(count: number): string
```

**Parameters**

- `count` `number`: How many digits to generate, an integer from 1 to 256.

**Returns** `string`: The digits.

**Throws**

- `INVALID_DIGIT_COUNT`: The digit count must be an integer from 1 to 256.

**Example**

```ts
import { generateRandomDigits } from 'jg-crypto';

generateRandomDigits(6); // such as '042917'
```

#### <a id="api-hashValue"></a>`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.

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

**Parameters**

- `value` `string | 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_ALGORITHM`: The hash algorithm must be sha256, sha384, or sha512.

**Example**

```ts
import { hashValue } from 'jg-crypto';

hashValue('abc'); // 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad'
hashValue('abc', 'sha512'); // a 128-character digest
```

#### <a id="api-signHmac"></a>`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.

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

**Parameters**

- `message` `string | 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.
- `secret` `string | 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_SECRET`: The HMAC secret must be a non-empty string or byte array.
- `INVALID_HMAC_DOMAIN`: The HMAC domain must be a non-empty string when it's given.

**Example**

```ts
import { signHmac } from 'jg-crypto';

const signature = signHmac('{"page":2}', secret, { domain: 'cursor' }); // 64 hex characters
```

#### <a id="api-verifyHmac"></a>`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.

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

**Parameters**

- `message` `string | Uint8Array<ArrayBufferLike>`: The string or bytes that were signed.
- `signature` `string`: The signature to check, 64 lowercase hex characters.
- `secret` `string | 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_SECRET`: The HMAC secret must be a non-empty string or byte array.
- `INVALID_HMAC_DOMAIN`: The HMAC domain must be a non-empty string when it's given.

**Example**

```ts
import { verifyHmac } from 'jg-crypto';

const isAuthentic = verifyHmac(rawBody, signatureHeader.replace(/^sha256=/, ''), webhookSecret);
```

### <a id="api-constants"></a>Constants

#### <a id="api-ERRORS"></a>`ERRORS`

*Constant*

the codes for every error thrown by the package

```ts
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" }
```

### <a id="api-types"></a>Types

#### <a id="api-IHashAlgorithm"></a>`IHashAlgorithm`

*Type*

the SHA-2 algorithms hashValue computes

```ts
type IHashAlgorithm = 'sha256' | 'sha384' | 'sha512';
```

#### <a id="api-IHmacOptions"></a>`IHmacOptions`

*Type*

the options accepted by signHmac and verifyHmac

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

**Members**

- `domain: string` (property): 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.

#### <a id="api-IRandomBytesEncoding"></a>`IRandomBytesEncoding`

*Type*

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

```ts
type IRandomBytesEncoding = 'base64url' | 'hex';
```
