Secret key generator
A high-entropy random secret for application configuration, generated locally and never sent over the network.
More options
Start with the page that matches what you are configuring
A generic secret works for most things, but several frameworks have a specific convention: a length their tooling produces, a format their loader expects, or a rotation path that has to be followed in a particular order. Those are covered on their own pages, which say what the value signs and what breaks when it changes.
| You are configuring | Page | What the value has to be |
|---|---|---|
| Django SECRET_KEY | /django-secret-key-generator/ | 50 characters by convention, signs sessions and reset links |
| A JWT signing secret | /jwt-secret-generator/ | At least 256 bits for HS256, per RFC 7518 |
| WordPress keys and salts | /wordpress-salt-generator/ | Eight separate values, one per authentication scheme |
| A symmetric encryption key | /encryption-key-generator/ | Raw bytes at 128, 192 or 256 bits |
| An AES key specifically | /aes-key-generator/ | 16, 24 or 32 bytes, plus a nonce that is not the key |
| An API key for your own service | /api-key-generator/ | Alphanumeric, prefixed, stored as a hash |
The two kinds of secret, which have different requirements
Most values called a secret key do one of two jobs, and confusing them causes real problems. A signing secret is HMAC key material: the application proves that a value it issued has not been altered. A session cookie, a signed URL, a password reset token and a JWT are all signatures. Anyone holding the signed value can read it, so nothing in it is private.
An encryption key is different. It keeps the content unreadable, and it has a size fixed by the algorithm rather than chosen by you. AES-256 takes exactly 32 bytes and will not take 31 or 33.
A signing secret has no fixed length, which is why a general-purpose generator serves it well. An encryption key does, which is why it deserves its own page with a byte count rather than a character count.
How many bits, and where the thresholds come from
The default here is 48 characters over an alphabet of 74, which is 298.05 bits. That is deliberately generous: for a value that is pasted once into a configuration file and never typed, length costs nothing, and the guidance below already stops mattering well before this point.
The boundaries in the table are anchored to citations rather than to intuition. They are the same bands the entropy readout uses across this site.
| Bits | Band | Basis |
|---|---|---|
| Under 32 | Broken | Roughly 2 billion guesses. A single GPU covers that range in milliseconds against a fast hash. |
| 32 to 63 | Weak | NIST SP 800-63B-4 requires a secret below 64 bits to be rate-limited. It is not considered safe standing alone. |
| 64 to 79 | Moderate | NIST's floor for recovery codes and for secrets that carry no rate limit. Fine for a short-lived link. |
| 80 to 111 | Strong | Session tokens and most API keys sit here. |
| 112 to 127 | Very strong | 112 bits is NIST's mandated minimum random bit generator security strength, via SP 800-131A. |
| 128 and above | Cryptographic | Matches AES-128. Beyond this, more bits buy nothing against brute force. |
These bands describe resistance to guessing the secret. They say nothing about how the secret is stored. A 298-bit key in a committed .env file is a 0-bit key.
The alphabet matters more than the length
A secret usually has to survive several layers of parsing before it reaches the code that uses it, and each layer has characters it treats specially. This is where generated secrets actually fail, and it is the reason the symbol set here is restricted rather than maximal.
The twelve symbols used by default are !#$%*+-=?@^_ , chosen because every one of them appears in the Microsoft Entra ID, Windows Active Directory Passfilt and OWASP allow-lists. The question mark is the one exception: it is absent from the AWS IAM non-alphanumeric list, so an IAM password wants the eleven-symbol preset instead. The default set gives an alphabet of 74 and 6.2095 bits per character, which is only 5.3% below the full 94-character printable ASCII set. The difference is one extra character every eighteen, and it removes the characters most likely to break something.
Even so, check the destination. Many .env loaders treat an unquoted hash as the start of a comment and some perform variable expansion on a dollar sign. YAML gives meaning to a leading percent sign, ampersand, asterisk and several others. A shell will expand a dollar sign or a backtick inside double quotes. If the secret is going anywhere near a config file you did not write the parser for, generate it as hex or base64url and stop worrying.
- Quote the value in .env files, and prefer single quotes where the loader supports them.
- For YAML, quote it and avoid leaving a symbol as the first character.
- For a shell, single quotes only. Double quotes still expand $ and backticks.
- For JSON, a backslash and a double quote need escaping; neither is in the default set.
Where the secret lives afterwards
The generator can only make the value unguessable. Everything after that is handling, and handling is where secrets are lost.
Keep it out of the repository, including the initial commit, which is where framework-generated secrets usually end up. Read it from the environment or from a secret manager, and let the application fail loudly at startup when it is absent rather than falling back to a default. A default is how a development secret reaches production.
Use a different value in every environment. Sharing one secret between staging and production means a staging compromise is a production compromise, and staging is always the less defended of the two. If a secret was ever committed, rotate it rather than rewriting history: the old value is already in every clone, every fork and every CI cache.
Doing this in your own code
JavaScript (browser and Node 19+)
const ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
function apiKey(length = 32) {
const n = ALPHABET.length; // 62
const limit = Math.floor(2 ** 32 / n) * n; // discard at or above this
const buf = new Uint32Array(length);
crypto.getRandomValues(buf);
let out = "";
for (let i = 0; i < length; i++) {
let v = buf[i];
while (v >= limit) v = crypto.getRandomValues(new Uint32Array(1))[0];
out += ALPHABET[v % n];
}
return out;
}
"sk_live_" + apiKey(); Mapping raw bytes onto 62 symbols with a bare % is where modulo bias gets introduced. The discard loop is what removes it, and at 62 symbols it fires roughly once in a billion draws.
Python 3.6+
import secrets, string
# token_urlsafe(n) takes a BYTE count, not a character count.
secrets.token_urlsafe(32) # 32 bytes -> 256 bits -> 43 characters
secrets.token_hex(16) # 16 bytes -> 128 bits -> 32 characters
# Exactly 32 base62 characters. choice() is backed by randbelow(),
# which rejects out-of-range draws instead of taking a modulo.
ALPHABET = string.ascii_letters + string.digits
"".join(secrets.choice(ALPHABET) for _ in range(32)) The random module is a Mersenne Twister and its internal state is recoverable from its output. It must never appear in this code path.
Go
import (
"crypto/rand"
"encoding/base64"
"math/big"
)
// 32 random bytes as base64url with no padding: 43 characters, 256 bits.
b := make([]byte, 32)
rand.Read(b)
token := base64.RawURLEncoding.EncodeToString(b)
// Exactly 32 base62 characters.
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
out := make([]byte, 32)
for i := range out {
n, _ := rand.Int(rand.Reader, big.NewInt(int64(len(alphabet))))
out[i] = alphabet[n.Int64()]
} crypto/rand, never math/rand. rand.Int is already unbiased; it rejects internally so you do not have to.
Rust
// rand = "0.9"
use rand::distr::{Alphanumeric, SampleString};
// 32 characters of A-Za-z0-9 from the thread CSPRNG (ChaCha12,
// seeded from the operating system and reseeded periodically).
let key = Alphanumeric.sample_string(&mut rand::rng(), 32);
// Raw bytes straight from the OS, for when the value is a key rather
// than a printable token. In rand 0.9 OsRng is fallible, so it offers
// try_fill_bytes rather than fill_bytes.
use rand::rngs::OsRng;
use rand::TryRngCore;
let mut k = [0u8; 32];
OsRng.try_fill_bytes(&mut k).expect("OS RNG unavailable"); PHP 7+
// 32 bytes as hex: 64 characters, 256 bits.
bin2hex(random_bytes(32));
// base64url, padding stripped: 43 characters.
rtrim(strtr(base64_encode(random_bytes(32)), '+/', '-_'), '=');
// Exactly 32 base62 characters. random_int() is unbiased.
$alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
$key = '';
for ($i = 0; $i < 32; $i++) {
$key .= $alphabet[random_int(0, strlen($alphabet) - 1)];
} random_bytes() and random_int() throw when the platform cannot supply entropy. mt_rand() and rand() return something wrong instead, which is why they are unusable here.
Java 8+
import java.security.SecureRandom;
import java.util.Base64;
SecureRandom rng = new SecureRandom();
byte[] raw = new byte[32];
rng.nextBytes(raw);
String token = Base64.getUrlEncoder().withoutPadding().encodeToString(raw);
// Exactly 32 base62 characters. nextInt(bound) is unbiased.
String ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
StringBuilder sb = new StringBuilder(32);
for (int i = 0; i < 32; i++) {
sb.append(ALPHABET.charAt(rng.nextInt(ALPHABET.length())));
} C# / .NET
using System.Security.Cryptography;
// 32 bytes as base64url with no padding.
byte[] raw = RandomNumberGenerator.GetBytes(32);
string token = Convert.ToBase64String(raw)
.TrimEnd('=').Replace('+', '-').Replace('/', '_');
// Exactly 32 base62 characters. GetString rejects internally. .NET 8 and later.
const string Alphabet =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
string key = RandomNumberGenerator.GetString(Alphabet, 32); Shell
# 32 bytes as hex: 64 characters.
openssl rand -hex 32
# base64url, padding stripped: 43 characters.
openssl rand -base64 32 | tr '+/' '-_' | tr -d '='
# 32 base62 characters, straight from the kernel CSPRNG.
LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 32; echo The tr pipeline discards every byte outside the alphabet rather than folding it back in, which is rejection sampling under another name, so the output is uniform.
Questions
How long should a secret key be?
For a signing secret, 32 random bytes is enough for anything, and there is no benefit above it beyond matching a convention your framework expects. For an encryption key the length is not yours to choose: the algorithm fixes it, and 32 bytes means exactly 32 bytes.
Can I use the same secret for signing and encryption?
No. Use one key for one purpose. Reusing key material across two algorithms means a weakness in either exposes both, and it makes rotation impossible to reason about because you can no longer say what a change will break. If you need several keys from one root value, derive them with HKDF using a distinct info string per purpose.
Is a passphrase acceptable as a secret key?
Not for a machine-to-machine secret. A phrase you can remember has far less entropy than it has characters, and nothing about a config file requires it to be memorable. Where a human genuinely has to type or read out the value, use a passphrase generator and accept the cost knowingly rather than by accident.
Does the secret ever leave my browser?
No. Generation happens in page JavaScript using crypto.getRandomValues(), and no request is made during it. Open the network panel and generate: it stays empty. Seven of the fifteen generators audited during research produce their values on a server, which means the secret exists on a machine you do not control before it reaches you.
What should I do if a secret has leaked?
Rotate it first and investigate afterwards. Deleting the commit, the message or the log line does not help, because you cannot know who already read it. Once the new value is live, work out what was signed or encrypted under the old one and whether any of it is still trusted.