Summary
This RFC proposes one plaintext format for all Unkey-generated API keys, including customer API keys and Unkey root keys. Imported and existing keys retain their original plaintext. This RFC supersedes the proposal in the historical key shape RFC.Motivation
The currently generated keys are not unkey-branded in a way that would let us write a regular expression to detect them in source control systems. This only works for root keys right now, as they have a constantunkey_ prefix, but it does not work for our customer’s keys.
Detailed design
Format
The general grammar is as follows.[x] indicates the number of characters.
1. The version occupies one Base58 character andincrements in Base58 alphabet order. It must appear before every version-dependent
field, so a parser can select the payload definition before reading its lengths. I do not like the fact, that the version is not at the very beginning or very end of the key. That would make parsing and versioning it simpler.
But the version is - by design - not random and if it’s part of the shown data (first 4 chars or 4 last chars) it doesn’t help users to see a difference between keys.
Version 1
Version 1 has this exact shape:
The complete key is between 59 and 65 characters.
It provides 256 random bits (as recommended by NIST) and base58 encodes that into 44 characters, split into an 8 char and a 36 char section. Prefixes, markers, and checksums add no entropy.
Prefix
Prefixes must match:prod_sk, pk_live, and unkey are valid.
prod_sk_ or eight-character prefixes are invalid.
Checksum
GitHub recommends adding a checksum, so we can pre-filter false positives without a database lookup. We calculate a CRC-32 checksum over the complete key up to, but excluding, the checksum:GitHub secret scanning
The version 1 regex for github is:Database changes
Separately I do want to change how we display keys in our dashboard. I think it would be nice to show not only the first 4 chars, but also the last 4. This just helps to visually discriminate between two keys when looking at them at a glance. To do that, we’d store the prefix, first four random characters, and final four characters for newly generated keys:prod_sk_K7pQ...X2Ks.
Existing non-recoverable keys cannot backfill end. Legacy rows keep their
existing start, and use "" for empty fields.
Storing the prefix like this, also makes it easier to reroll keys, cause we don’t have to parse the key to figure out what the prefix should be.
Deprecating configurable lengths
We should remove the option to choose a custom length and just follow the NIST recommendation of 256 bits of entropy. There’s no good reason for us to allow a user to use less and having fewer (mostly irrelevant) config options is always good in my opinion.Drawbacks
- New keys are a little longer.
Alternatives
- Put
unkeyv1at the beginning: easier to scan, but customer keys become harder to distinguish visually and appear Unkey-branded. - Put the version at the end: keeps the body visually clean, but a parser cannot know version-dependent payload and checksum lengths before reaching it.
- Omit the checksum: shortens keys, but always requires database lookups for secret scanning false positives.