> ## Documentation Index
> Fetch the complete documentation index at: https://engineering.unkey.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 0017 API key plaintext format

> Define a versioned, fixed-entropy plaintext format for generated API keys

## 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](/architecture/rfcs/0003-key-shape).

## 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 constant `unkey_` 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.

```plaintext theme={"theme":"kanagawa-wave"}
{prefix[1-7]}_{random[8]}unkeyv{version[1]}{depending on the version}
```

Version numbering starts at `1`. The version occupies one Base58 character and\
increments 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.

```
// bad because you can't visually scan the prefix and figure out what's what
sk_unke...131f 
sk_unke...0ann


// bad because you can't visually scan the suffix and figure out what's what
sk_9agx...eyv1 
sk_Z0Lk...eyv1
```

I chose to put it after 8 chars of randomness. This works, but also means that this is now fixed for eternity. A new format must also embed its version after the first 8 random chars, otherwise we couldn't parse a key reliably if we ever wanted to. In reality this is probably fine, I do not foresee a reason to parse a key's version other than to figure out if it has a checksum, and since we allow users to import their own keys, we'd never be able to do this for their keys anyways. But I would like to reserve the option...

### Version 1

Version 1 has this exact shape:

```plaintext theme={"theme":"kanagawa-wave"}
{prefix[1-7]}_{random[8]}unkeyv1{random[36]}{checksum[6]}
```

| Field              | Length | Contents                       |
| ------------------ | -----: | ------------------------------ |
| Prefix             |    1–7 | User controlled prefix         |
| Separator          |      1 | `_`                            |
| Random head        |      8 | First part of the random value |
| Marker and version |      7 | Literal `unkeyv1`              |
| Random tail        |     36 | Rest of the random value       |
| Checksum           |      6 | Fixed-width Base58 CRC-32C     |

The complete key is between 59 and 65 characters.

It provides 256 random bits (as recommended by [NIST](https://pages.nist.gov/800-63-4/sp800-63b.html)) 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:

```plaintext theme={"theme":"kanagawa-wave"}
^[A-Za-z0-9_]{0,6}[A-Za-z0-9]$
```

They contain one through seven ASCII letters, numbers, or underscores, and must
end in an alphanumeric character. `prod_sk`, `pk_live`, and `unkey` are valid.
`prod_sk_` or eight-character prefixes are invalid.

#### Checksum

GitHub [recommends](https://docs.github.com/code-security/secret-scanning/secret-scanning-partnership-program/secret-scanning-partner-program#identify-your-secrets-and-create-regular-expressions) 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:

```plaintext theme={"theme":"kanagawa-wave"}
unsigned_key   = prefix + "_" + random_head + "unkeyv1" + random_tail
checksum_value = CRC-32C(ASCII(unsigned_key))
checksum_text  = base58Fixed(checksum_value, 6)
key            = unsigned_key + checksum_text
```

### GitHub secret scanning

The version 1 regex for github is:

```plaintext theme={"theme":"kanagawa-wave"}
[A-Za-z0-9_]{0,6}[A-Za-z0-9]_[1-9A-HJ-NP-Za-km-z]{8}unkeyv1[1-9A-HJ-NP-Za-km-z]{42}
```

Legacy root keys keep their existing GitHub pattern. Either we can provide 2 expressions, or we just merge them for one mega-cursed one.

### 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:

```plaintext theme={"theme":"kanagawa-wave"}
prefix = "prod_sk"
start  = "K7pQ"
end    = "X2Ks"
```

And then display them as `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 `unkeyv1` at 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.
