Skip to content

BitwoHQ/nuxt-cryptography

Nuxt Cryptography banner

Nuxt Cryptography

npm version npm downloads License Nuxt

Secure, framework-native cryptography utilities that work identically on the server and in the browser.

Features

  • 🔐  Universal Web Crypto wrapper with automatic Node/browser fallbacks
  • 🧩  useCrypto composable injected in every Nuxt app (SSR + client)
  • 🛠️  Server helper useServerCrypto for Nitro routes and API handlers
  • ♻️  Stateless AES-GCM encryption/decryption, UUIDs, secure random bytes, and SHA digests
  • 🔑  Built-in Argon2 password generator + verifier with PHC-formatted output
  • 🧮  HMAC helpers powered by Web Crypto for request signing and webhooks

Quick Setup

Install the module and register it in your nuxt.config:

# npm
npx nuxi@latest module add @bitwo.io/nuxt-cryptography

# bun
bunx nuxi@latest module add @bitwo.io/nuxt-cryptography
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@bitwo.io/nuxt-cryptography'],
})

Configure secrets via .env

  1. Add a strong symmetric key (≥12 chars) to your project .env file:

    NUXT_CRYPTO_SECRET="my-ultra-long-secret-change-me"
    # Optional fallbacks also supported out-of-the-box
    CRYPTO_SECRET="my-ultra-long-secret-change-me"
  2. (Optional) Expose an alternative key name by passing cryptography.secretEnvKey in nuxt.config if your team already standardised a different variable.

  3. Restart Nuxt so runtimeConfig.cryptography.secret picks up the new value.

Secrets defined in .env are only readable on the server unless you explicitly copy them to runtimeConfig.public. Avoid exporting encryption secrets to the browser unless you fully understand the implications.

Client + SSR usage

<script setup lang="ts">
const crypto = useCrypto()
const secret = 'choose-a-long-secret'

const hash = await crypto.hash('hello world')
const encrypted = await crypto.encrypt('message', secret)
const decrypted = await crypto.decrypt(encrypted, secret)
</script>

Server usage (Nitro endpoints, server routes)

export default defineEventHandler(async (event) => {
  const crypto = useServerCrypto(event)
  const encrypted = await crypto.encrypt('payload', process.env.SERVER_SECRET!)

  return {
    hash: await crypto.hash('server'),
    decrypted: await crypto.decrypt(encrypted, process.env.SERVER_SECRET!),
  }
})

Note

useServerCrypto lives in runtime/server, so it must always receive the current event from your handler. This avoids importing client-side composables inside Nitro code, as recommended by the Nuxt module guide.

Argon2 password generator

Hash credentials with RFC 9106-compliant Argon2 (default: argon2id) directly from the same composables:

const crypto = useCrypto()
const password = 'CorrectHorseBatteryStaple!'

const result = await crypto.passwordGenerator(password, {
  iterations: 3,
  memorySize: 64 * 1024, // kibibytes
  parallelism: 1,
  hashLength: 32,
  algorithm: 'argon2id',
})

console.log(result.encoded)
// => $argon2id$v=19$m=65536,t=3,p=1$...

Key details:

  • Defaults follow the Argon2 RFC (t=3, m=64 MiB, p=1, 32-byte hash) and automatically generate a 16-byte random salt.
  • Pass your own Uint8Array or base64/string salt to make outputs deterministic, e.g. during tests or migrations.
  • The helper returns both the PHC string (encoded) and individual pieces (hash, salt, params) so you can store whichever format your system prefers.
  • Available anywhere createUniversalCrypto is used, including server routes via useServerCrypto and standalone utilities.

Verifying Argon2 hashes

Use verifyPassword whenever you need to validate user input against a stored PHC string (e.g. outputs from passwordGenerator or existing Argon2 hashes):

const crypto = useServerCrypto(event)
const isValid = await crypto.verifyPassword(candidatePassword, storedHash)

if (!isValid) {
  throw createError({ statusCode: 401, statusMessage: 'Invalid credentials' })
}

HMAC helpers

Sign payloads (webhooks, API requests, cache keys) with the same secret handling as the AES utilities:

const crypto = useCrypto()
const signature = await crypto.hmac(JSON.stringify(body), {
  algorithm: 'SHA-512',
  // secret optional when runtimeConfig.cryptography.secret is set
})

headers['x-signature'] = signature

Outputs are lowercase hex digests so you can compare them safely with timingSafeEqual on the server.

Bare utility access

When you need cryptography outside of Nuxt/Nitro contexts you can create a standalone instance:

import { createUniversalCrypto } from '@bitwo.io/nuxt-cryptography'

const crypto = createUniversalCrypto()

Note

Secrets used with the AES helpers must be at least 12 characters to ensure minimum entropy.

Default secret fallback

If you configure a secret via runtimeConfig.cryptography.secret (or expose one with the NUXT_CRYPTO_SECRET / CRYPTO_SECRET environment variables), the secret argument on crypto.encrypt/crypto.decrypt becomes optional:

export default defineNuxtConfig({
  modules: ['@bitwo.io/nuxt-cryptography'],
  runtimeConfig: {
    cryptography: {
      secret: process.env.NUXT_CRYPTO_SECRET,
    },
  },
})

const cipher = await useServerCrypto(event).encrypt('payload')

You can customize the env keys the module inspects by setting cryptography.secretEnvKey (string or array). Set runtimeConfig.public.cryptography.secret only if you intentionally need a default secret on the client—remember that exposing symmetric secrets in the browser is insecure.

Tune the cryptography parameters

All cryptographic knobs exposed by the module ship with secure defaults but can be overridden when needed (for example, Cloudflare Workers reject PBKDF2 iterations above 100000). Configure them directly under the cryptography key in nuxt.config:

export default defineNuxtConfig({
  modules: ['@bitwo.io/nuxt-cryptography'],
  cryptography: {
    secretEnvKey: ['NUXT_CRYPTO_SECRET', 'CRYPTO_SECRET'],
    pbkdf2Iterations: 100_000,
    saltLength: 16,
    ivLength: 12,
    keyLength: 256,
  },
})

These values flow into runtimeConfig (and the public runtime config for non-secret values) so both the server and browser share the same derivation settings.

Option reference

Option Default Notes
secret undefined Directly define the symmetric key used when encrypt/decrypt are called without arguments. Min length: 12 chars. Prefer .env + runtimeConfig over hard-coding.
secretEnvKey ['NUXT_CRYPTO_SECRET', 'CRYPTO_SECRET'] Ordered list of env vars inspected during module setup. Accepts string or string[].
saltLength 16 bytes Prepended to every ciphertext. Must be a positive integer.
ivLength 12 bytes AES-GCM IV length. Must be ≥12 to satisfy Web Crypto requirements.
pbkdf2Iterations 210_000 PBKDF2 iteration count. Lower if your platform enforces an upper limit (e.g. 100_000 on Cloudflare Workers).
keyLength 256 bits AES-GCM key size. Must be one of 128, 192, or 256.

All numeric options are validated during startup—misconfigured values fail fast with descriptive errors, making it easier to debug environment-specific policies like the PBKDF2 cap you encountered.

Contribution

Local development
# Install dependencies
npm install        # or: bun install

# Generate type stubs
npm run dev:prepare        # or: bun run dev:prepare

# Develop with the playground
npm run dev                # or: bun run dev

# Build the playground
npm run dev:build          # or: bun run dev:build

# Run ESLint
npm run lint               # or: bun run lint

# Run Vitest
npm run test               # or: bun run test
npm run test:watch         # or: bun run test:watch

# Type-safety regression suite
npm run test:types         # or: bun run test:types

# Release new version
npm run release            # or: bun run release

See CONTRIBUTING.md for detailed workflow expectations (branching, reviews, release process, and security policies).

Commit Messages

Use clear, concise commit messages and follow Conventional Commits when possible:

  • feat: add new AES-256 helper
  • fix: correct PBKDF2 parameter validation
  • docs: update usage examples
  • test: add tests for deriveKey
  • chore: internal cleanup
  • ci: fix GitHub Actions cache key

Security

If you discover a vulnerability, please email security@bitwo.io instead of opening a public issue. Include reproduction steps, environment details, and any mitigation ideas so we can coordinate a fix and disclosure timeline together.

About

Secure, framework-native cryptography utilities that work identically on the server and in the browser.

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Languages