Secure, framework-native cryptography utilities that work identically on the server and in the browser.
- 🔐 Universal Web Crypto wrapper with automatic Node/browser fallbacks
- 🧩
useCryptocomposable injected in every Nuxt app (SSR + client) - 🛠️ Server helper
useServerCryptofor 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
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'],
})-
Add a strong symmetric key (≥12 chars) to your project
.envfile: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"
-
(Optional) Expose an alternative key name by passing
cryptography.secretEnvKeyinnuxt.configif your team already standardised a different variable. -
Restart Nuxt so
runtimeConfig.cryptography.secretpicks 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.
<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>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.
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
Uint8Arrayor 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
createUniversalCryptois used, including server routes viauseServerCryptoand standalone utilities.
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' })
}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'] = signatureOutputs are lowercase hex digests so you can compare them safely with timingSafeEqual on the server.
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.
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.
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 | 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.
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 releaseSee CONTRIBUTING.md for detailed workflow expectations (branching, reviews, release process, and security policies).
Use clear, concise commit messages and follow Conventional Commits when possible:
feat: add new AES-256 helperfix: correct PBKDF2 parameter validationdocs: update usage examplestest: add tests for deriveKeychore: internal cleanupci: fix GitHub Actions cache key
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.