How to Generate a Secure API Key in JavaScript Using Crypto.getRandomValues

Published March 9, 2026 · 6 min read

If you need to generate API keys in JavaScript, you have exactly one correct approach: crypto.getRandomValues(). This Web Crypto API method provides cryptographically secure pseudo-random numbers (CSPRNG) that are suitable for security-sensitive operations like key generation, token creation, and secret management.

This guide walks you through the complete process, from understanding why Math.random() is dangerous to building a production-ready key generator with proper entropy.

Why Not Math.random()?

Math.random() uses a deterministic PRNG algorithm (typically xorshift128+ in V8) that was designed for statistical simulations, not security. Its output is predictable if an attacker can observe enough values, and it provides only about 52 bits of entropy per call. For API keys that protect your infrastructure, this is unacceptable.

crypto.getRandomValues() draws from the operating system's entropy pool (/dev/urandom on Linux, BCryptGenRandom on Windows) and is considered cryptographically secure. Every modern browser and Node.js 15+ supports it.

Step 1: Generate Random Bytes

The foundation of any secure key is a buffer of random bytes. Here is how to generate one:

// Generate 32 random bytes (256 bits of entropy)
const bytes = new Uint8Array(32);
crypto.getRandomValues(bytes);
// bytes is now filled with cryptographically random values 0-255

The Uint8Array(32) creates a typed array of 32 bytes, giving you 256 bits of entropy. This exceeds the minimum 128-bit recommendation for API keys and provides a comfortable security margin against brute-force attacks.

Step 2: Convert Bytes to a Hex String

Hex encoding is the simplest and most common format for API keys. Each byte becomes two hexadecimal characters:

function generateHexKey(byteLength = 32) {
  const bytes = new Uint8Array(byteLength);
  crypto.getRandomValues(bytes);
  return Array.from(bytes)
    .map(b => b.toString(16).padStart(2, '0'))
    .join('');
}

// Output: "a3f8c1d9e4b7...64 characters total"
console.log(generateHexKey());

A 32-byte input produces a 64-character hex string with 256 bits of entropy. For a shorter key with 128 bits of entropy, pass 16 as the byte length.

Step 3: Generate Base64 Keys

Base64 encoding is more compact than hex. The same 32 bytes produce a 44-character string instead of 64:

function generateBase64Key(byteLength = 32) {
  const bytes = new Uint8Array(byteLength);
  crypto.getRandomValues(bytes);
  return btoa(String.fromCharCode(...bytes))
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=+$/, '');
}

// Output: "p9jB2eS3...about 43 characters"
console.log(generateBase64Key());

The replacements at the end convert standard Base64 to URL-safe Base64 (RFC 4648), which is critical if your keys will appear in URLs, query parameters, or HTTP headers.

Step 4: Generate Alphanumeric Keys

Some systems require keys that contain only letters and numbers. Here is a rejection-sampling approach that maintains uniform distribution:

function generateAlphanumericKey(length = 32) {
  const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  const bytes = new Uint8Array(length);
  crypto.getRandomValues(bytes);
  let result = '';
  for (let i = 0; i < length; i++) {
    result += chars[bytes[i] % chars.length];
  }
  return result;
}

// Output: "Kx9mR4vL2bN7...32 characters"
console.log(generateAlphanumericKey());

Note: Using modulo introduces a slight bias since 256 is not evenly divisible by 62. For most API key use cases this bias is negligible. For maximum uniformity, use rejection sampling by discarding values >= 248 (the largest multiple of 62 below 256).

Step 5: Add a Prefix for Environment Safety

Following the Stripe convention, you can prepend a prefix that identifies the key's environment and type:

function generatePrefixedKey(prefix = 'sk_live', byteLength = 24) {
  const bytes = new Uint8Array(byteLength);
  crypto.getRandomValues(bytes);
  const key = Array.from(bytes)
    .map(b => b.toString(16).padStart(2, '0'))
    .join('');
  return `${prefix}_${key}`;
}

// Output: "sk_live_a3f8c1d9e4b7..."
console.log(generatePrefixedKey());
console.log(generatePrefixedKey('sk_test'));

Prefixes help prevent accidental use of production keys in development, enable quick identification during log review, and allow secret scanners like GitHub's to detect leaked keys.

Minimum Entropy Requirements

The entropy of your key determines its resistance to brute-force attacks. Here is a practical reference:

At 128 bits, an attacker attempting one trillion guesses per second would need approximately 5.4 x 10^15 years to exhaust the keyspace. At 256 bits, the number becomes incomprehensibly large.

Node.js Implementation

In Node.js, you can use the same Web Crypto API or the built-in crypto module:

import { randomBytes } from 'node:crypto';

function generateApiKey(byteLength = 32) {
  return randomBytes(byteLength).toString('hex');
}

// Or using Web Crypto API (Node 19+)
function generateApiKeyWebCrypto(byteLength = 32) {
  const bytes = new Uint8Array(byteLength);
  crypto.getRandomValues(bytes);
  return Buffer.from(bytes).toString('hex');
}

Both methods draw from the same OS-level entropy source and are equally secure.

Production Best Practices

  1. Never generate keys on the client side for server authentication. Generate them on the server and transmit them over HTTPS.
  2. Store only hashes. Hash keys with SHA-256 or bcrypt before storing them in your database. Display the full key exactly once at creation time.
  3. Set expiration dates. Rotate keys periodically (90 days is a common interval) and support multiple active keys during rotation.
  4. Use environment-specific prefixes. Prevent test keys from working in production and vice versa.
  5. Log key prefixes, not full keys. If you need to trace API calls, log only the first 8 characters.
  6. Rate-limit key creation. Prevent abuse by limiting how many keys a user can generate per hour.

Complete Production-Ready Function

function createApiKey(options = {}) {
  const {
    prefix = '',
    byteLength = 32,
    format = 'hex' // 'hex' | 'base64' | 'alphanumeric'
  } = options;

  const bytes = new Uint8Array(byteLength);
  crypto.getRandomValues(bytes);

  let key;
  switch (format) {
    case 'base64':
      key = btoa(String.fromCharCode(...bytes))
        .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
      break;
    case 'alphanumeric':
      const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
      key = Array.from(bytes).map(b => chars[b % chars.length]).join('');
      break;
    default:
      key = Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
  }

  return prefix ? `${prefix}_${key}` : key;
}

// Usage
createApiKey({ prefix: 'sk_live', byteLength: 32, format: 'hex' });
createApiKey({ format: 'base64', byteLength: 24 });
createApiKey({ prefix: 'tok', format: 'alphanumeric', byteLength: 20 });

This function covers all common API key formats with proper entropy, URL-safe encoding, and optional prefixes. Use it as a starting point and adapt it to your specific security requirements.

For instant key generation without writing code, try our free API key generator which uses the same crypto.getRandomValues() approach right in your browser.

Recommended Resources

Want to master JavaScript's crypto APIs? JavaScript: The Definitive Guide is the comprehensive reference, covering the Web Crypto API and typed arrays used throughout this article.

For the cryptographic theory behind getRandomValues(), Real-World Cryptography explains CSPRNGs, entropy sources, and why they matter.

SPUNK LLC Network

API Sites

Key Management

More from SPUNK LLC