---
title: "Encryption Key Generator: AES-256, Fernet and Hex Keys"
description: "Generate AES-128/192/256 keys in hex or base64, Fernet keys, random hex strings and AES-GCM nonces. Made in your browser with Web Crypto, never sent."
image: "https://cdn.sethserver.com/blog-images/encryption-key-generator-social-1789657010.webp"
---

# Encryption Key Generator: AES-256, Fernet and Hex Keys

Key type

Length

Unit

Uppercase hex

Pick a key type, copy the encoding your code expects, done. Keys come from `crypto.getRandomValues` in your browser, which draws on your operating system's random number generator, and nothing is sent to a server. For keys that will protect real data, I would still rather you run one of the offline commands further down on the machine that needs the key. Fewer places a key has been, fewer places it can leak from.

Looking for something that signs rather than encrypts? The [secret key generator](https://www.sethserver.com/security/secret-key-generator.html) handles framework secrets (Flask, Django, Laravel, Rails and friends), the [Django secret key generator](https://www.sethserver.com/security/django-secret-key-generator.html) matches Django's format exactly, and the [JWT secret generator](https://www.sethserver.com/security/jwt-secret-generator.html) and [API key generator](https://www.sethserver.com/security/api-key-generator.html) cover those.

## Which key do you need?

| You are using | Pick | Size |
| --- | --- | --- |
| AES-GCM in Python, Node, Go, Java, .NET, or Web Crypto | AES-256, hex or base64 | 32 bytes |
| Python `cryptography.fernet.Fernet` (Airflow's `fernet_key` setting uses the same format) | Fernet key | 32 bytes, 44 characters |
| A config value that says "random hex, N characters" | Random hex string, unit hex characters | whatever it asks for |
| Testing AES-GCM code by hand | AES-256 key plus a nonce | 32 + 12 bytes |
| Anything that says "128-bit key" | AES-128 | 16 bytes |

An AES key has no structure. It is just random bytes of the right length: 16 bytes for AES-128, 24 for AES-192, 32 for AES-256. Hex and base64 are only ways of writing those bytes down as text. Your code has to decode them back to bytes before handing them to the cipher. Passing the 64 character hex string where 32 bytes are expected is one of the most common bugs I see, and some libraries will not even complain.

## A key is not a password

A 32 byte random key has 256 bits of entropy. A good human password has maybe a fraction of that, and a typical one has far less. If you encrypt with `sha256("hunter2")` as the key, the ciphertext is exactly as strong as "hunter2", and an attacker with the ciphertext can guess offline as fast as their hardware allows.

When a human has to supply the secret, run it through a **password-based key derivation function**. KDFs are deliberately slow and, in the better ones, memory hungry, so each guess costs the attacker real work:

- **Argon2id**: the modern default. The `cryptography` library's Fernet docs recommend it for deriving keys from passwords, with parameters based on RFC 9106.
- **scrypt**: memory hard, widely available (Node has `crypto.scrypt` built in, Python has `hashlib.scrypt`).
- **PBKDF2**: the oldest and weakest of the three against GPU attackers, but available everywhere and often the one compliance regimes list. Use a high iteration count.

Every one of them needs a random **salt**, generated per password and stored next to the ciphertext. The salt is not secret. It exists so that two people with the same password end up with different keys.

If no human is involved (a service encrypting data, a backup job, an app encrypting a database column), skip passwords entirely and use a random key from this page, `openssl rand`, or your KMS.

## AES-GCM is the sensible default

If you are picking a cipher today and nobody is handing you requirements, use AES-256-GCM. GCM is an **authenticated** mode: it encrypts and also produces a tag that detects any change to the ciphertext. Decryption with a modified ciphertext, the wrong key, or the wrong nonce fails loudly instead of returning garbage. Older setups like AES-CBC need a separate MAC bolted on, and getting that composition right is where a lot of real-world bugs have come from.

GCM needs three things per message:

- **The key**: 16, 24 or 32 bytes. Secret. Reused across many messages.
- **A nonce (IV)**: 12 bytes is the standard size. The `cryptography` docs note that NIST recommends a 96-bit IV. Not secret. **Never reused with the same key.**
- **Optional associated data**: bytes that get authenticated but not encrypted, like a record ID, so a ciphertext cannot be swapped onto a different row.

Here is the full round trip in Python with the `cryptography` package:

```
import base64, os
from cryptography.hazmat.primitives.ciphers.aead import AESGCM

key = base64.b64decode(os.environ["DATA_KEY"])   # 32 bytes
aesgcm = AESGCM(key)

nonce = os.urandom(12)                            # new for EVERY message
ciphertext = aesgcm.encrypt(nonce, b"attack at dawn", b"record-42")

stored = nonce + ciphertext                       # nonce travels with the data

plaintext = aesgcm.decrypt(stored[:12], stored[12:], b"record-42")
```

And the same thing in Node, where you handle the tag yourself:

```
const crypto = require("crypto");

const key = Buffer.from(process.env.DATA_KEY, "base64");  // 32 bytes
const iv = crypto.randomBytes(12);                        // new for EVERY message

const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
const ciphertext = Buffer.concat([cipher.update("attack at dawn", "utf8"), cipher.final()]);
const tag = cipher.getAuthTag();                          // 16 bytes

const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
decipher.setAuthTag(tag);
const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8");
```

## About the nonce on this page

The nonce mode exists for testing, test vectors and poking at code by hand. In a real program you generate the nonce inside the encrypt function, every single time, as in the examples above. The cryptography docs are blunt about why: reusing a nonce with the same key "compromises the security of any message with that nonce and key pair." With GCM that means an attacker can recover the XOR of the two plaintexts and can start forging messages. A nonce copied from a web page into a config file gets reused by definition.

Nonces are not secret. Store them in front of the ciphertext, as above. Hiding them buys nothing and makes decryption annoying.

## Fernet keys

Fernet is the "hard to misuse" recipe in Python's `cryptography` library. A Fernet key is 32 random bytes written as URL-safe base64 with padding, which always comes out to 44 characters ending in `=`. This is `Fernet.generate_key()`, straight from the library source:

```
@classmethod
def generate_key(cls) -> bytes:
    return base64.urlsafe_b64encode(os.urandom(32))
```

Internally, Fernet splits the key in half: the first 16 bytes are the HMAC-SHA256 signing key and the last 16 bytes are the AES-128-CBC encryption key. Each token also carries a timestamp, so you can reject tokens older than a TTL on decrypt.

```
from cryptography.fernet import Fernet

f = Fernet(os.environ["FERNET_KEY"])
token = f.encrypt(b"my deep dark secret")
f.decrypt(token)              # b"my deep dark secret"
f.decrypt(token, ttl=3600)    # also fails if the token is older than an hour
```

A Fernet key is **not** interchangeable with a plain base64 AES key. The encoding uses `-` and `_` instead of `+` and `/`, and the bytes are split into two keys. Use the Fernet mode for Fernet and the AES modes for everything else.

## Generate encryption keys offline

```
# AES-256 key (use 16 for AES-128, 24 for AES-192)
openssl rand -hex 32
openssl rand -base64 32

# Random hex string of N bytes (2N characters)
openssl rand -hex 20

# 12 byte AES-GCM nonce, for testing
openssl rand -hex 12

# Python
python3 -c "import secrets; print(secrets.token_hex(32))"
python3 -c "import os, base64; print(base64.b64encode(os.urandom(32)).decode())"

# Fernet key
python3 -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
openssl rand -base64 32 | tr '+/' '-_'

# Node
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
```

The `tr` trick works because a Fernet key is standard base64 with two characters swapped, and 32 bytes of base64 always ends in a single `=` either way.

To check a key decodes to the size you think it does:

```
python3 -c "import base64, sys; print(len(base64.b64decode(sys.argv[1])))" 'YOUR_BASE64_KEY'
python3 -c "import sys; print(len(bytes.fromhex(sys.argv[1])))" YOUR_HEX_KEY
```

## Where encryption keys should live

The golden rule: **not next to the data they protect**. An encrypted database backup with the key in the same bucket is an unencrypted backup with extra steps.

In rough order of preference:

1. **A key management service** (AWS KMS, Google Cloud KMS, Azure Key Vault, HashiCorp Vault's transit engine). The master key never leaves the service, every use is logged, and access is controlled by IAM policy.
2. **A secrets manager** (AWS Secrets Manager, Google Secret Manager, Vault's KV store, Doppler, 1Password for teams). The app fetches the key at startup, and access is logged and revocable.
3. **Environment variables** injected by your platform. Fine for a lot of apps, but they tend to leak into crash dumps, `/proc`, debug pages and child processes, so keep an eye on what inherits them.

Never in the repository, never in a Docker image layer, never in a log line, and never in a ticket or chat message "just for a minute." Keep production keys separate from development keys, and give each service its own key so one compromise stays one compromise. I wrote more about scoping access in [the principle of least privilege in the AI age](https://www.sethserver.com/ai/principle-of-least-privilege-in-the-ai-age.html).

## Key rotation and envelope encryption

Encryption keys are harder to rotate than signing keys, because data encrypted with the old key stays encrypted with it until you re-encrypt it. The usual pattern:

1. Tag every ciphertext with a key ID (a byte or a short prefix is enough).
2. Add the new key and start encrypting with it. Keep the old key for decryption only.
3. Re-encrypt old records in the background, or lazily whenever they are read.
4. Retire the old key once nothing references it.

Fernet has this built in: `MultiFernet([Fernet(new), Fernet(old)])` encrypts with the first key, tries each key on decrypt, and `rotate(token)` re-encrypts a token under the first key.

**Envelope encryption** is how the big systems make rotation cheap. Each object gets its own random *data key*. You encrypt the object with the data key, then encrypt the data key with a *key encryption key* held in a KMS, and store the wrapped data key alongside the object. To rotate the master key you only re-wrap small data keys, not terabytes of data, and the master key itself never leaves the KMS. AWS KMS's `GenerateDataKey` call exists for exactly this: it hands back the data key in plaintext for immediate use and in encrypted form for storage.

If you are building this for a single app with a single key, you probably do not need envelope encryption yet. Tag your ciphertexts with a key ID from day one anyway. It is the one decision that is painful to add later.

## Common mistakes

- **Hardcoding the nonce.** See above. Generate it per message.
- **Using ECB mode.** Identical plaintext blocks produce identical ciphertext blocks. If your library defaults to ECB, that is a sign to use a different API.
- **Hashing a password into a key with plain SHA-256.** Use a KDF.
- **Passing the hex text instead of the decoded bytes.** 64 hex characters are 32 bytes only after decoding.
- **Rolling your own format.** If Fernet, libsodium's secretbox, or your cloud provider's encryption SDK fits, use it and move on.

For the bigger picture, here are [my opinionated security best practices](https://www.sethserver.com/security/seths-opinionated-security-best-practices.html).

## Encryption Key FAQ

How do I generate an AES-256 key?

An AES-256 key is 32 random bytes from a cryptographically secure random number generator. Pick AES-256 above and copy it as hex (64 characters) or base64 (44 characters). Offline, openssl rand -hex 32 or openssl rand -base64 32 produce the same thing.

Should I use AES-128 or AES-256?

Both are secure. AES-256 is the more common default, is often required by compliance rules, and leaves a larger safety margin, while AES-128 is slightly faster. If nothing forces your hand, use AES-256 in GCM mode. AES-192 works but is rarely used, and some libraries do not support it.

Can I use a password as an encryption key?

Not directly. A password has far less entropy than a random key and is not the right length. Run it through a password-based key derivation function such as Argon2id, scrypt or PBKDF2 with a random salt, and store the salt next to the ciphertext.

Does the IV or nonce need to be secret?

No. An AES-GCM nonce is stored or sent alongside the ciphertext in the clear. What matters is that the same nonce is never used twice with the same key. Generate a fresh 12 byte nonce for every message in your code, rather than reusing one from this page.

What is a Fernet key?

A Fernet key is 32 random bytes encoded as URL-safe base64 with padding, which is 44 characters ending in an equals sign. Python's cryptography library uses the first 16 bytes as an HMAC-SHA256 signing key and the last 16 bytes as an AES-128-CBC encryption key.

Is a random hex string the same as an encryption key?

A hex string of 32 random bytes decodes to a valid AES-256 key, so yes if your code decodes the hex first. The common bug is passing the 64 character hex text itself as the key, which some libraries reject and others quietly treat as 64 bytes of ASCII.

Is it safe to generate encryption keys in a browser?

The keys on this page come from crypto.getRandomValues, which is the operating system random number generator exposed to the browser, and nothing is sent to a server. For production keys, generating them where they will live, with openssl, your language's secrets module, or a KMS, removes the browser and clipboard from the picture entirely.

Related tools: [secret key generator](https://www.sethserver.com/security/secret-key-generator.html), [Django secret key generator](https://www.sethserver.com/security/django-secret-key-generator.html), [Flask secret key generator](https://www.sethserver.com/security/flask-secret-key-generator.html), [JWT secret generator](https://www.sethserver.com/security/jwt-secret-generator.html), [API key generator](https://www.sethserver.com/security/api-key-generator.html), and the [random passphrase generator](https://www.sethserver.com/security/strong-random-passphrase-generator.html) for the passwords you feed a KDF.

-Sethers


```json
{"@context": "https://schema.org", "@graph": [{"@type": "WebApplication", "name": "Encryption Key Generator", "url": "https://www.sethserver.com/security/encryption-key-generator.html", "description": "Generate AES-128, AES-192 and AES-256 keys in hex or base64, random hex strings, Fernet keys and AES-GCM nonces in your browser with the Web Crypto API.", "applicationCategory": "SecurityApplication", "operatingSystem": "Web", "author": {"@type": "Person", "name": "Seth Black"}, "offers": {"@type": "Offer", "price": "0", "priceCurrency": "USD"}}, {"@type": "FAQPage", "mainEntity": [{"@type": "Question", "name": "How do I generate an AES-256 key?", "acceptedAnswer": {"@type": "Answer", "text": "An AES-256 key is 32 random bytes from a cryptographically secure random number generator. Pick AES-256 above and copy it as hex (64 characters) or base64 (44 characters). Offline, openssl rand -hex 32 or openssl rand -base64 32 produce the same thing."}}, {"@type": "Question", "name": "Should I use AES-128 or AES-256?", "acceptedAnswer": {"@type": "Answer", "text": "Both are secure. AES-256 is the more common default, is often required by compliance rules, and leaves a larger safety margin, while AES-128 is slightly faster. If nothing forces your hand, use AES-256 in GCM mode. AES-192 works but is rarely used, and some libraries do not support it."}}, {"@type": "Question", "name": "Can I use a password as an encryption key?", "acceptedAnswer": {"@type": "Answer", "text": "Not directly. A password has far less entropy than a random key and is not the right length. Run it through a password-based key derivation function such as Argon2id, scrypt or PBKDF2 with a random salt, and store the salt next to the ciphertext."}}, {"@type": "Question", "name": "Does the IV or nonce need to be secret?", "acceptedAnswer": {"@type": "Answer", "text": "No. An AES-GCM nonce is stored or sent alongside the ciphertext in the clear. What matters is that the same nonce is never used twice with the same key. Generate a fresh 12 byte nonce for every message in your code, rather than reusing one from this page."}}, {"@type": "Question", "name": "What is a Fernet key?", "acceptedAnswer": {"@type": "Answer", "text": "A Fernet key is 32 random bytes encoded as URL-safe base64 with padding, which is 44 characters ending in an equals sign. Python's cryptography library uses the first 16 bytes as an HMAC-SHA256 signing key and the last 16 bytes as an AES-128-CBC encryption key."}}, {"@type": "Question", "name": "Is a random hex string the same as an encryption key?", "acceptedAnswer": {"@type": "Answer", "text": "A hex string of 32 random bytes decodes to a valid AES-256 key, so yes if your code decodes the hex first. The common bug is passing the 64 character hex text itself as the key, which some libraries reject and others quietly treat as 64 bytes of ASCII."}}, {"@type": "Question", "name": "Is it safe to generate encryption keys in a browser?", "acceptedAnswer": {"@type": "Answer", "text": "The keys on this page come from crypto.getRandomValues, which is the operating system random number generator exposed to the browser, and nothing is sent to a server. For production keys, generating them where they will live, with openssl, your language's secrets module, or a KMS, removes the browser and clipboard from the picture entirely."}}]}]}
{"@context": "https://schema.org", "@type": "BreadcrumbList", "itemListElement": [{"@type": "ListItem", "position": 1, "name": "sethserver", "item": "https://www.sethserver.com/"}, {"@type": "ListItem", "position": 2, "name": "Tools", "item": "https://www.sethserver.com/tools/"}, {"@type": "ListItem", "position": 3, "name": "Encryption Key Generator: AES-256, Fernet and Hex Keys", "item": "https://www.sethserver.com/security/encryption-key-generator.html"}]}
```
