# Secret values

A secret value is a password, key, token or code that your app receives
and checks, but never sends, logs, stores or shows. It arrives in a
request's `Input`, or through `text.parse` or `json.read`, and the
compiler keeps it from going any further. There are seven:

| Type | What it holds |
|---|---|
| `Password` | 8 to 256 characters with no control character; any other Unicode and spaces are allowed. 8 is NIST SP 800-63B's minimum. Raise it with `min_length:`; NIST asks for 15 where a password is the only factor |
| `ApiKey` | 1 to 512 printable ASCII characters with no spaces, whatever prefix the provider uses |
| `BearerToken` | RFC 6750's `b64token`, at most 8,192 characters |
| `Jwt` | A JSON Web Token, which `crypto.verify_jwt` checks. Its text is RFC 6750's `b64token`, at most 8,192 characters |
| `TotpSecret` | RFC 4648 Base32 in upper case without padding, in its one spelling, holding 16 to 64 bytes (at least RFC 4226's 128 bits) |
| `SecretText` | 1 to 8,192 characters of anything, line breaks included, so it has room for a PEM key |
| `OneTimeCode` | The code a person types: six to eight ASCII digits (at least RFC 4226's six, at most RFC 6238's eight), a leading zero kept |

None of them is ordered: your code can't compare one with `<` or sort
by it. Sets and maps still keep secrets in an order of their own, which
[Comparing secrets](secret-values.md#comparing-secrets) describes.

Here a sign-up form takes a password, which goes no further than its
hash, and a partner's API key is recognised by what is kept of it:

```jadpo
type MemberHandle = Text {
    max_length: 40
}

type ChosenPassword = Password {
    min_length: 15
}

/// What the sign-up form sends: the password arrives, and goes no further
/// than its hash.
type SignUpForm = Input {
    handle: MemberHandle
    chosen_password: ChosenPassword
}

type KeyVerifier = Bytes {}

type Recognised = Bool {}

/// The hash a new member's password is kept as.
action password_to_keep(sign_up: SignUpForm) -> PasswordHash {
    return crypto.hash_password(sign_up.chosen_password)
}

/// What is kept of a partner's API key: its SHA-256, which signs no one
/// in.
function key_verifier(partner_key: ApiKey) -> KeyVerifier {
    return KeyVerifier(hash.sha256(partner_key))
}

/// Whether a key a partner sends is the one it was issued.
function recognised(offered_key: ApiKey, kept_verifier: KeyVerifier) -> Recognised {
    return Recognised(key_verifier(offered_key) == kept_verifier)
}
```

```jadpo
test "a password is kept as its hash" {
    var sign_up: SignUpForm = SignUpForm {
        handle: "ada"
        chosen_password: "correct horse battery staple"
    }
    var kept_hash: PasswordHash = password_to_keep(sign_up)
    assert crypto.verify_password(sign_up.chosen_password, kept_hash) == PasswordCheck.correct
}

test "a partner's key is recognised by its verifier" {
    var kept_verifier: KeyVerifier = key_verifier("example_key_0123456789abcdef")
    assert recognised("example_key_0123456789abcdef", kept_verifier)
    assert not recognised("example_key_0123456789abcdeX", kept_verifier)
}
```

## Where a secret can go

A secret stays on the server. An `Input`, an `Object`, a variable and a
parameter may hold one, and `json.read` reads one, since none of them
leaves it.

Nothing that leaves the server may hold one, however deeply it is
nested in lists, maps, options, objects, `Input`s or a variant's data.
The compiler refuses a secret in each of these with
`type.secret_exposed`:

- an `Output`;
- a failure's `client` or `internal` fields. A failure's fields are
  logged, so this keeps secrets out of the logs too;
- an event, which is kept in the outbox and handed on;
- an email or a message.

`json.write` can't write one either (`type.mismatch`). A `PasswordHash`
has a rule of its own, `type.password_hash_exposed`, because a hash,
unlike a secret, is stored.

## How a secret is kept

A secret is never stored as itself. Each kind is kept as something that
does the same job:

- A password is kept as its `PasswordHash`.
- An API key or a bearer token is kept as `hash.sha256` of it, as
  [Identity](starting-and-ending-a-session.md#api-tokens) keeps a token's verifier.
- A `Jwt` is checked when it arrives and never kept.
- A one-time code is kept only as the time it was used.
- A `TotpSecret`, which checking a code needs back, and a `SecretText`
  are stored sealed with an encrypted field's key
  ([Encrypted fields](encrypted-fields.md)).

An entity's field that holds any other secret is `type.secret_exposed`,
and the error's `next:` line names what to keep instead. Keeping the
password itself is refused:

```jadpo
type MemberHandle = Text {
    max_length: 40
}

entity Member {
    id: Uuid
    handle: MemberHandle {
        unique: true
    }
    chosen_password: Password
}
```

```text
error: `Member.chosen_password` keeps a `Password`
 --> main.jadpo:10:22
   |
10 |     chosen_password: Password
   |                      ^^^^^^^^ a secret, stored
  reason: A `Password` is never stored as itself, so a copy of the database holds none. A password is hashed with `crypto.hash_password` and checked with `crypto.verify_password`.
  next: Store `crypto.hash_password(...)` of it in a `PasswordHash` field in `chosen_password`'s place (agent)
  rule: type.secret_exposed
  help: https://jadpo.dev/docs/errors/type.secret_exposed

check failed: 1 error, 1 file
```

## What may read a secret

Only the operation made for a secret reads it:

- `crypto.hash_password` and `crypto.verify_password` take a `Password`.
- `crypto.verify_jwt` takes a `Jwt`.
- `hash.sha256` takes an `ApiKey` or a `BearerToken`. It never takes a
  `Password`, whose only hash is Argon2id's, nor a `OneTimeCode`, since
  the digest of a six-digit code is found in a million tries.

Everything else that would read a secret is `type.secret_exposed`, so no
length, part, text or format for people can carry one out:

- any other operation, one over a collection of secrets included, so
  `text.join` can't join them;
- any operator but `==` and `!=`, since an order or `matches` would
  answer questions about the text;
- `text.from` and `text.format`;
- building any type except one built on the same secret.

Building a type that is built on it is allowed, as in
`StaffPassword(password)`.

## Comparing secrets

`==` and `!=` on secrets take a time that depends on the two values'
lengths alone, so how long a comparison takes says nothing of where two
values differ. Every byte up to the longer value's length is compared,
and the answer is decided only once all of them have been, so the
comparison never stops early. This is how
[Identity](starting-and-ending-a-session.md#api-tokens) compares its verifiers, and a
secret compared with text written in source is compared the same way.

A set or a map orders secrets by a comparison that also looks at every
byte.

A secret value equals its text wherever the two meet, in time that
depends on the lengths alone: inside a list, a set, a map's keys and
values, an object and an enum's data, however deep
(`[p] == ["hunter2hunter2"]`). In the order sets and maps keep, a
secret value sits among text values, ordered by its text. So
`set.contains` finds a secret by its text, and finds text by an equal
secret, and that order always agrees with `==`.

## Reports show a secret redacted

Wherever Jadpo shows a value, it shows a secret as `<redacted ApiKey>`,
naming its type: a failed test's values, `jadpo test`'s output and its
`--json`, a failure's report, a fault, and anything a debugger or
`jadpo watch` shows. Every one of them writes a value the same way.

A test that fails over a secret shows it redacted wherever it shows a
value, and its diff still says which field differs:

```jadpo
type PartnerName = Text {
    max_length: 100
}

type PartnerLogin = Object {
    name: PartnerName
    partner_key: ApiKey
}

/// A partner, signed in with the key it sent.
function signed_in(name: PartnerName, offered_key: ApiKey) -> PartnerLogin {
    return PartnerLogin {
        name: name
        partner_key: offered_key
    }
}
```

```jadpo
test "a partner keeps the key it signed in with" {
    var issued_key: ApiKey = "example_key_0123456789abcdef"
    assert signed_in("Northwind", issued_key) == PartnerLogin {
        name: "Northwind"
        partner_key: "example_key_9876543210fedcba"
    }
}
```

```text
FAIL  a partner keeps the key it signed in with
 --> main.test.jadpo:3

  assert signed_in("Northwind", issued_key) == PartnerLogin { ...
         |                      |              |
         |                      |              PartnerLogin { name: "Northwind", partner_key: <redacted ApiKey> }
         |                      <redacted ApiKey>
         PartnerLogin { name: "Northwind", partner_key: <redacted ApiKey> }

  expected: PartnerLogin { name: "Northwind", partner_key: <redacted ApiKey> }
  actual: PartnerLogin { name: "Northwind", partner_key: <redacted ApiKey> }
  diff: ~ partner_key: expected <redacted ApiKey>, actual <redacted ApiKey>

  rule: test.assertion_failed
  rerun: jadpo test main.test.jadpo:3 --seed 1

test failed: 1 failed, 1 pure test
```

## A setting that holds a secret

A setting whose type is a secret value follows its `secret:` flag, as
every setting does ([Secrets](secrets.md)).
With `secret: true`, it goes only to places the compiler owns. Without
it, your code reads it as a secret value, which the rules above keep in
and show redacted.

## Recordings for replay

A recording of a request, kept so it can be replayed
([What the graph and production evidence add](what-the-graph-and-production-evidence-add.md)),
holds no credential the runtime made. The bytes the runtime makes a
session, an API token, a service key or a CSRF token from are never kept
on a recording's tape, nor taken from a replayed one, so a replay makes
fresh ones. The program's own [`random`](randomness.md) draws still replay
in order.

No `crypto.encrypt` nonce is kept either. It is drawn as a credential's
bytes are, so a replay never seals with a nonce that a production run
used under the same key. A password's salt isn't secret and stays on the
tape, so a replayed `hash_password` gives the hash the recorded run made.

## OpenAPI

OpenAPI describes a secret value as a `writeOnly` string, and a
`Password` with `format: password` as well, so generated clients hide it
as it is typed.

> Not built yet: storing a `TotpSecret` or a `SecretText` needs keys held
> as data ([Language gaps this page depends on](language-gaps-this-page-depends-on.md)).
> Until that is built, storing one is `toolchain.not_built_yet` rather
> than refused. No operation reads a `OneTimeCode` yet: `crypto.verify_totp`
> waits for keys held as data. `jadpo replay`, which the recordings above
> are for, isn't built; when it is, it keeps out the draws a secret value
> was made from.
