KeyGenerator.io Blog

How to Generate an AES-256 Key

An AES-256 key is 32 random bytes, which is 64 hex characters or 44 Base64 characters once encoded. Generate it with OpenSSL (openssl rand -hex 32) or Node.js (randomBytes(32)), then use the encoding your application expects.

Open the AES key generator

Raw bytes or encoded text?

Check two things before you generate anything: does your configuration expect raw bytes or encoded text, and if encoded, which encoding? A correctly generated key still fails when it is pasted in the wrong format.

Prefer a browser? Open the AES key generator, leave 256-bit selected, choose Hex or Base64, and copy the result. To generate a key in your own development environment, use the OpenSSL or Node.js commands below. Wherever you generate it, keep the key out of tickets, chat messages and logs.

Generate an AES-256 key with OpenSSL

Run this in a terminal where OpenSSL is installed:

Bash
openssl rand -hex 32

The final number is a byte count. The command prints 32 random bytes as 64 hexadecimal characters, followed by a newline. The newline is terminal formatting and is not part of the key.

If your application requires Base64, use:

Bash
openssl rand -base64 32

The encoded value has 44 characters, including padding. Each command generates a new key, so these two commands produce different keys: they show alternative formats and are not conversions of one key.

Do not use openssl rand -hex 256 for AES-256. It requests 256 bytes, not 256 bits.

To keep the key out of your terminal scrollback, create a new file with owner-only permissions. The command below refuses to overwrite an existing file. On a normal Unix filesystem, umask 077 restricts the permissions of the newly created file; it does not change permissions on existing files. Strip the trailing newline when you load the value:

Bash
(umask 077; set -C; openssl rand -hex 32 > aes256.key)

See the OpenSSL rand documentation for the byte-count and encoding options.

Generate an AES-256 key with Node.js

Save the following as generate-key.mjs and run node generate-key.mjs:

JavaScript
import { randomBytes } from 'node:crypto';

const key = randomBytes(32);
console.log('Hex:', key.toString('hex'));
console.log('Base64:', key.toString('base64'));

Both lines print the same key, because the random bytes are generated once. Keep whichever representation your application accepts.

Printing is acceptable in this one-off local script. Never print keys in application or deployment logs.

randomBytes() draws on Node.js's cryptographically secure random source. Use it for key material, never Math.random(), a timestamp, or a string you made up.

AES-256 key length and encoding

The number in AES-128, AES-192 and AES-256 is the key size in bits, as defined by the AES standard. When you validate a key, count decoded bytes rather than visible characters.

AES key lengths, with padded Base64
AlgorithmBytesHex charactersBase64 characters (padded)
AES-128163224
AES-192244832
AES-256326444

A 32-character hex string is 16 bytes, which is an AES-128 key. For AES-256 in hex you need 64 characters.

Hex and Base64 are two spellings of the same bytes. Converting between them does not make a key stronger, so pick the one your configuration format and encryption library support.

Load the key correctly

Suppose you saved the key in an environment variable such as AES_KEY_HEX (hex) or AES_KEY_B64 (Base64). Save this helper as load-aes-key.mjs. It checks the encoding, validates the text, and decodes exactly 32 bytes:

JavaScript
import { Buffer } from 'node:buffer';
import { createSecretKey } from 'node:crypto';

export function loadKey(name, encoding) {
  if (encoding !== 'hex' && encoding !== 'base64') {
    throw new Error('Supported encodings are hex and base64');
  }

  const value = process.env[name]?.trim();
  const pattern = encoding === 'hex'
    ? /^[0-9a-fA-F]{64}$/
    : /^[A-Za-z0-9+/]{43}=$/;
  if (!value || !pattern.test(value)) {
    throw new Error(`${name} must be a 32-byte key encoded as ${encoding}`);
  }

  const bytes = Buffer.from(value, encoding);
  if (bytes.length !== 32) {
    throw new Error(`${name} must decode to exactly 32 bytes`);
  }
  if (encoding === 'base64' && bytes.toString('base64') !== value) {
    throw new Error(`${name} must use canonical padded Base64`);
  }
  return createSecretKey(bytes);
}

The validation is deliberate. Node.js decoding can silently truncate invalid hex input, and a successful Base64 decode does not prove every input character was valid. Re-encoding also rejects noncanonical Base64 representations. The trim() tolerates surrounding whitespace, including a terminal newline.

Do not call Buffer.from(encoded) without an encoding. It reads the hex characters as UTF-8 text, so a 64-character value becomes 64 bytes instead of 32, and AES-256 fails with an Invalid key length error.

Encrypt with AES-256-GCM

A key alone is not encryption. Use an authenticated mode such as GCM to protect confidentiality and detect modifications. Avoid ECB, which reveals repeated plaintext blocks. CBC requires a correctly designed authentication scheme; prefer an authenticated mode instead of assembling one yourself.

Save the following as aes-gcm-example.mjs in the same directory as load-aes-key.mjs. This complete example imports the helper and stores the 12-byte IV, 16-byte authentication tag, and ciphertext in one buffer:

JavaScript
import { Buffer } from 'node:buffer';
import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';
import { loadKey } from './load-aes-key.mjs';

export function encrypt(key, plaintext) {
  const iv = randomBytes(12);
  const cipher = createCipheriv('aes-256-gcm', key, iv, {
    authTagLength: 16,
  });
  const ciphertext = Buffer.concat([
    cipher.update(plaintext, 'utf8'),
    cipher.final(),
  ]);
  return Buffer.concat([iv, cipher.getAuthTag(), ciphertext]);
}

export function decrypt(key, payload) {
  if (!Buffer.isBuffer(payload) || payload.length < 28) {
    throw new Error('Payload must contain a 12-byte IV and a 16-byte tag');
  }
  const iv = payload.subarray(0, 12);
  const tag = payload.subarray(12, 28);
  const ciphertext = payload.subarray(28);
  const decipher = createDecipheriv('aes-256-gcm', key, iv, {
    authTagLength: 16,
  });
  decipher.setAuthTag(tag);
  const plaintext = Buffer.concat([
    decipher.update(ciphertext),
    decipher.final(),
  ]);
  return plaintext.toString('utf8');
}

const key = loadKey('AES_KEY_HEX', 'hex');
// For Base64, use loadKey('AES_KEY_B64', 'base64') instead.
const payload = encrypt(key, 'Hello, AES-GCM.');
console.log(decrypt(key, payload)); // Hello, AES-GCM.

If you created aes256.key with the earlier command, run this in a POSIX-compatible shell from the directory containing all three files:

Bash
AES_KEY_HEX="$(cat aes256.key)" node aes-gcm-example.mjs

The example prints the recovered demonstration message, not the key. Its IV + tag + ciphertext layout is an example format, not a universal standard. A persisted application format should also identify its version and the key needed for decryption.

Three rules matter more than the code:

  • Never reuse an IV with the same key. In GCM, reuse can compromise confidentiality and authentication. This example generates a new random IV for every message, subject to the per-key usage limit below.
  • The IV is not secret. Store it next to the ciphertext. It is a separate parameter from the key.
  • Reject the message if authentication fails. An incorrect key, corrupted data, or tampering can cause failure. Do not use any partial plaintext before authentication succeeds. See the Node.js authentication-tag documentation.

For the random-IV construction used here, NIST SP 800-38D, section 8.3 caps authenticated-encryption invocations at 2^32 per key, counted across all instances using that key. This is an upper bound, not a guarantee that randomly selected IVs cannot collide. Set a lower operational limit and rotate before reaching it.

Derive a key from a password

If the secret is a password or passphrase, never use it directly as a key, and do not pad or hash it by hand. Run it through a password-based key derivation function: scrypt or Argon2id where available, PBKDF2 where a standard requires it. Node.js includes scrypt:

Save this separate helper as derive-key.mjs:

JavaScript
import { randomBytes, scrypt } from 'node:crypto';
import { promisify } from 'node:util';

const scryptAsync = promisify(scrypt);

export async function deriveKey(
  password,
  salt = randomBytes(16),
  params = { N: 2 ** 17, r: 8, p: 1 },
) {
  const key = await scryptAsync(password, salt, 32, {
    N: params.N,
    r: params.r,
    p: params.p,
    maxmem: 256 * 1024 * 1024,
  });
  return { key, salt, params: { ...params }, kdf: 'scrypt' };
}

Generate a fresh random salt when creating a new password-derived key. The salt is not secret. Store its encoded value, the KDF name, the N/r/p parameters, the derived-key length (32 bytes), and a format version alongside the ciphertext. Keep the derived key secret. To reproduce it, decode the saved salt and call deriveKey(password, salt, savedParams) with the same password and parameters.

The cost settings above require roughly 128 MiB of working memory per derivation, plus overhead. Runtime depends on the hardware and concurrent load: benchmark your deployment environment before choosing parameters. Only accept stored parameters within bounds your application allows. See the Node.js scrypt documentation.

A derived key is only as strong as the password behind it. A short or common password stays easy to guess however good the key size is.

Store and rotate the key

Generate the key once when you provision the application, then load the saved value at startup. Where it lives decides how safe it is.

  • Secret manager. Keep keys in your platform's secret store, such as AWS Secrets Manager, Google Secret Manager, Azure Key Vault or HashiCorp Vault. Limit who can read them, and keep them out of source control, container images and logs.
  • Separate environments. Use different keys for development and production.
  • Envelope encryption. For higher assurance, let a KMS hold a master key. Your application generates a random 32-byte data key per file or record, encrypts the data with it, and stores the data key wrapped by the KMS beside the data. If your migration requires re-wrapping, you can re-wrap those small data keys under a replacement KMS key without re-encrypting the bulk data. This is a separate operation from automatic rotation of KMS key material: AWS KMS automatic rotation, for example, does not re-wrap existing data keys.
  • Rotation. A new key does not make old ciphertext readable. Store a key ID or version with every ciphertext, keep retired keys available for decryption until the data is re-encrypted, and test the migration before you rotate.
  • Recovery. Decide in advance who can retrieve the key and how it is backed up. Data encrypted with a lost key is gone.

Common mistakes

  • Regenerating the key on restart. Data encrypted before the restart can no longer be decrypted. Generate once, then load.
  • Confusing the key with the IV. The IV or nonce is a separate parameter with its own rules. Never copy the key into that field.
  • Reusing an example value. A key copied from a tutorial is known to everyone who read it. Generate your own.
  • Counting characters instead of bytes. A 32-character hex string is an AES-128 key. AES-256 needs 64 hex characters or 44 Base64 characters.
  • Logging the key. Keep it out of application logs, error messages and deployment output.

Need a key without writing code? The AES key generator creates 128, 192 and 256-bit keys in Hex or Base64. Match your application's key size and encoding, then check the result against the length table above. For token signing rather than encryption, use the JWT secret generator and follow your signing algorithm's requirements.

AES-256 key FAQ

How long is an AES-256 key?

32 bytes (256 bits). Encoded, that is 64 hex characters or 44 Base64 characters including padding.

How do I generate an AES-256 key?

Run openssl rand -hex 32, or call randomBytes(32) from node:crypto. Both use a cryptographically secure random source.

Should I use hex or Base64?

Either. They are two encodings of the same 32 bytes, and neither is stronger. Use the one your application or library expects.

Can I use a password as an AES-256 key?

Not directly. Derive a 32-byte key from it with scrypt, Argon2id or PBKDF2 and a random salt.

Is AES-256 better than AES-128?

AES-256 has a larger key space than AES-128. Choose the size required by your application or standard. Both AES-128 and AES-256 require careful key management and correct IV handling.