Encryption key generator
Symmetric encryption keys at 128, 192 or 256 bits, generated in your browser and shown in hex with the byte count.
Keys by bit size
A symmetric key is a fixed number of random bytes. The size is decided by the algorithm, not by you, and a cipher will simply refuse a key of the wrong length. The default here is 32 bytes, which is the size AES-256, ChaCha20 and HMAC-SHA-256 all take.
The table gives the three standard sizes in every unit you are likely to see them written in. It is worth reading the columns against each other, because the units are where mistakes happen: a 32-character hex string is 16 bytes, not 32, and the difference between those two readings is the difference between AES-128 and AES-256.
| Bits | Bytes | Hex characters | base64url characters | Used by |
|---|---|---|---|---|
| 128 | 16 | 32 | 22 | AES-128, the smallest size still considered sound |
| 192 | 24 | 48 | 32 | AES-192, rarely chosen, no practical advantage over 256 |
| 256 | 32 | 64 | 43 | AES-256, ChaCha20, HMAC-SHA-256. The default here. |
Hex doubles the length and base64 adds about a third. Neither adds a single bit. The key is the bytes; hex and base64 are only ways of writing them down.
A 256-bit key is not twice as strong as a 128-bit one
Every additional bit doubles the number of possible keys, so 256 bits is not double 128 bits, it is 2 to the power 128 times as many. That is a number with no physical meaning, and the framing matters because it points at the real conclusion: the gap is not the reason to choose 256.
The reason is that 128 is already unreachable. Measured against a single RTX 5090 running MD5 at 220.6 GH/s, exhausting a 128-bit space takes on the order of 10 to the power 19 years, and a farm of 100,000 such cards still needs roughly 244 trillion years. No amount of engineering closes a gap of that size, and AES-128 has no known attack that meaningfully reduces it.
There are honest reasons to pick 256 anyway. A compliance regime may name it, in which case the discussion is over. Grover's algorithm would halve the effective security of a symmetric key, taking 256 bits down to a 128-bit equivalent, which is why 256-bit symmetric keys are the standard recommendation for post-quantum durability, on hardware that does not currently exist. And it costs almost nothing, which is why the default here is 256. The dishonest reason is believing that 128 is weak, because it is not.
A key is bytes. A password is not a key.
The most common mistake in this area is taking a password or passphrase from a user, hashing it with SHA-256, and using the digest as an encryption key. It compiles, it produces 32 bytes, and it is badly broken.
The problem is that the key inherits the entropy of the password, not of the digest. A 30-bit password hashed to 256 bits is a 30-bit key, because the attacker guesses passwords rather than keys. SHA-256 makes that worse by being fast: research measured hashcat 6.2.6-851 doing SHA2-256 at 28.35 GH/s on a single RTX 5090. Guessing a human-chosen password at that rate is not a multi-year project.
The correct tool is a password-based key derivation function with a tunable work factor and a per-use salt. Argon2id is the current first choice, scrypt is a sound second, and PBKDF2-HMAC-SHA-256 with a high iteration count is what you use when a certification names it. The measured spread on identical hardware is the argument: 220.6 GH/s for MD5 against 7,760 H/s for scrypt at N=16384, which is seven orders of magnitude of attacker cost bought by choosing the right function. No Argon2 figure is quoted here, because hashcat 6.2.6 has no Argon2 mode and any number would be invented.
Two neighbouring functions are not substitutes. bcrypt is a password verifier with a fixed output and a 72-byte input limit; it is not built to produce key material. HKDF is for the opposite problem, expanding one already-uniform key into several independent ones, and it has no work factor, so pointing it at a password gives you a fast hash with extra steps.
Getting the bytes into your code without corrupting them
The failure here is quiet, which is what makes it dangerous. A 256-bit key written as hex is 64 characters. Read those 64 characters as UTF-8 text and you get 64 bytes, not 32. Some libraries reject that, some truncate it, and some accept it and derive a key from the wrong material entirely. Everything appears to work until another implementation, which decoded correctly, cannot read your ciphertext.
Decode explicitly at the boundary. Whichever encoding you store the key in, convert it to bytes in one place, assert the resulting length, and pass bytes around from then on. That is why this tool shows the byte count next to the character count rather than leaving you to divide.
Store the key somewhere designed for it: a secret manager, a KMS, or the platform keystore. An environment variable is acceptable and is a large improvement on a source file, but remember that it is visible to the whole process tree and is often captured in crash dumps and in process listings.
One key, one purpose
Use a separate key for each job: encrypting one class of data, encrypting another, signing tokens, signing URLs. Sharing key material across purposes means that a weakness or a leak in one place compromises all of them, and it makes rotation impossible to scope, because nobody can say what a change will affect.
If several keys have to come from one root value, derive them with HKDF using a distinct info string per purpose. That is what HKDF is for, and it gives you keys that are independent even though they share an origin.
Store a key identifier alongside every ciphertext. Rotation is impossible without it: to decrypt old data with an old key while writing new data with a new one, the ciphertext has to say which key it was made with. Adding that field later means rewriting every record you have.
Doing this in your own code
Shell
# 128-bit key: 16 bytes, 32 hex characters.
openssl rand -hex 16
# 256-bit key: 32 bytes, 64 hex characters.
openssl rand -hex 32
# The same 256-bit key in base64: 44 characters, the last of which is padding.
openssl rand -base64 32 A 64-character hex string is 32 bytes, not 64. Reading the text as UTF-8 and feeding those 64 bytes to a cipher is one of the most common key-handling bugs there is.
Node
import { randomBytes, createCipheriv, createDecipheriv } from "node:crypto";
const key = randomBytes(32); // AES-256. Bytes, not a string.
const iv = randomBytes(12); // 96-bit GCM nonce, fresh for every message.
const c = createCipheriv("aes-256-gcm", key, iv);
const ct = Buffer.concat([c.update(plaintext, "utf8"), c.final()]);
const tag = c.getAuthTag(); // 16 bytes
const d = createDecipheriv("aes-256-gcm", key, iv);
d.setAuthTag(tag);
const pt = Buffer.concat([d.update(ct), d.final()]); Store the nonce and the tag next to the ciphertext. Neither is secret and both are required to decrypt.
Python (cryptography)
import os
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
key = AESGCM.generate_key(bit_length=256) # 32 bytes
aes = AESGCM(key)
nonce = os.urandom(12) # never reuse this with this key
ct = aes.encrypt(nonce, b"plaintext", b"associated data")
pt = aes.decrypt(nonce, ct, b"associated data") encrypt() appends the 16-byte tag to the ciphertext, so only the nonce has to be stored separately.
Go
key := make([]byte, 32)
rand.Read(key) // crypto/rand
block, _ := aes.NewCipher(key)
gcm, _ := cipher.NewGCM(block) // 12-byte nonce
nonce := make([]byte, gcm.NonceSize())
rand.Read(nonce)
ct := gcm.Seal(nonce, nonce, plaintext, nil) // nonce prefixed to the output Deriving a key from a password
import hashlib, os
# WRONG. The key inherits the entropy of the password, not of the digest,
# and SHA-256 runs at tens of billions of guesses per second on one GPU.
key = hashlib.sha256(password.encode()).digest()
# RIGHT. A password-based KDF with a work factor and a per-user salt.
from argon2.low_level import hash_secret_raw, Type
key = hash_secret_raw(
secret=password.encode(), salt=os.urandom(16),
time_cost=3, memory_cost=64 * 1024, parallelism=4,
hash_len=32, type=Type.ID,
)
# HKDF is the other case: expanding one already-uniform key into several.
# It has no work factor, so it must never be pointed at a password. Questions
Should I use a 128-bit or a 256-bit key?
256 bits, unless something forces the choice. Both are far beyond brute force, but 256 is the usual default, costs almost nothing, and is the standard recommendation for symmetric keys against a future quantum attacker. Do not choose 128 because you think 256 is excessive, and do not assume 128 is unsafe, because it is not.
Can I use a password as an encryption key?
Not directly, and not by hashing it. Put it through a password-based KDF with a work factor and a per-use salt: Argon2id, scrypt, or PBKDF2 when a standard requires it. The derived key is still limited by the entropy of the password, so a KDF buys time rather than making a weak password strong.
Why is the key shown in hex rather than as text?
Because it is bytes, and hex is the least ambiguous way to write bytes down. Every hex character is exactly four bits, so the mapping is exact and reversible in one direction only. A key rendered as printable text invites the mistake of using the text itself as the key.
Is a key generated in a browser as good as one from openssl rand?
Yes. crypto.getRandomValues() draws from the same operating system CSPRNG that OpenSSL does. In Chromium the call routes through BoringSSL to ProcessPrng on Windows and getrandom() on Linux. Nothing generated here is transmitted, which you can verify from the network panel.
Do I need a new key for every message?
No, and that is what the nonce is for. One key encrypts many messages, and a fresh nonce or IV per message keeps them independent. What must never repeat under a single key is the nonce.