# Encryption

> Encrypt and sign values with the App's key, bind them to a purpose, let them expire, rotate keys, and look up encrypted values with blind indexes.

This guide covers encryption in Tinka. You will learn how to:

- Encrypt and decrypt values with the App's key
- Bind a value to its purpose, and make it expire
- Sign values that may be read but must not be changed
- Find encrypted values in a database with blind indexes
- Rotate the App's key without breaking existing values

## Overview

Encryption turns a value into text only your App can read back. Unlike a password hash, it's reversible: you encrypt a value to store or send it safely, and decrypt it when you need it. The `encryption` package (`github.com/mucolabs/tinka/encryption`) encrypts with your App's key, `APP_KEY`, and authenticates every value: one that was changed, made with another key, or made for another purpose fails to decrypt instead of returning garbage.

Values are encrypted with AES-256-GCM, under a key derived for each value from `APP_KEY`, so one key can encrypt any number of values safely. The output is URL-safe text, ready for a cookie, a link or a database column.

## Create an Encrypter

Create one `Encrypter` from the App's key when the App starts, and share it. `act generate:key` writes a key to your `.env` file, as the [configuration](../../configuration.md#the-app-key) guide shows:

```go title="main.go"
	e := env.Load()
	enc, err := encryption.New(e.Secret("APP_KEY"))
	if err != nil {
		return err // a missing or short key
	}
```

`New` fails for a key shorter than 32 characters, so a placeholder key never reaches production. An `Encrypter` is safe for concurrent use, and printing or logging it never shows its key.

## Encrypt and decrypt values

`Encrypt` encodes a value as JSON and encrypts it. `Decrypt` returns it as the type you name:

```go title="invites.go"
package main

import (
	"errors"

	"github.com/mucolabs/tinka"
	"github.com/mucolabs/tinka/encryption"
)

type Invite struct {
	Email  string `json:"email"`
	TeamID int64  `json:"team_id"`
}

type Invites struct {
	Enc *encryption.Encrypter
}

func (h Invites) Link(invite Invite) (string, error) {
	token, err := h.Enc.Encrypt(invite)
	if err != nil {
		return "", err
	}
	return "https://example.com/invites/" + token, nil
}

func (h Invites) Accept(c *tinka.Ctx) (Invite, error) {
	invite, err := h.Enc.Decrypt[Invite](c.Param("token"))
	if errors.Is(err, encryption.ErrInvalid) {
		return Invite{}, tinka.NotFound("This invitation link is not valid.")
	}
	return invite, err
}
```

`Decrypt` fails with `encryption.ErrInvalid` for any value it can't trust: changed, cut short, made with another key, or made for another purpose. One error covers every case, so a failure tells an attacker nothing about which check failed. Encrypting the same value twice gives different outputs, so equal values can't be spotted in a database.

## Bind values to a purpose

A purpose binds a value to what it's for. It decrypts only with the same purpose, so a token made for one use can't be replayed for another, even when both hold the same data:

```go
	token, err := enc.Encrypt(user.ID, encryption.Purpose("password-reset"))

	id, err := enc.Decrypt[int64](token, encryption.Purpose("password-reset")) // ok
	id, err = enc.Decrypt[int64](token, encryption.Purpose("email-verification")) // ErrInvalid
	id, err = enc.Decrypt[int64](token)                                          // ErrInvalid
```

## Make values expire

`ExpiresIn` gives a value a lifetime. Once it passes, `Decrypt` fails with `encryption.ErrExpired`, which you can tell apart from an invalid value to show a clearer message:

```go title="invites.go"
	token, err := h.Enc.Encrypt(invite, encryption.ExpiresIn(7*24*time.Hour), encryption.Purpose("invite"))

	invite, err := h.Enc.Decrypt[Invite](c.Param("token"), encryption.Purpose("invite"))
	switch {
	case errors.Is(err, encryption.ErrExpired):
		return Invite{}, tinka.NotFound("This invitation has expired. Ask for a new one.")
	case errors.Is(err, encryption.ErrInvalid):
		return Invite{}, tinka.NotFound("This invitation link is not valid.")
	}
```

The expiry is inside the encrypted value, so it can't be changed without breaking the value.

## Sign values without hiding them

`Sign` protects a value from changes without hiding it: anyone can read a signed value, but `Verify` fails for one that was changed. Use it for data that isn't secret but must be trusted when it comes back, such as the user ID in an unsubscribe link. `Sign` takes the same `Purpose` and `ExpiresIn` options:

```go
	link, err := enc.Sign(user.ID, encryption.Purpose("unsubscribe"))

	id, err := enc.Verify[int64](token, encryption.Purpose("unsubscribe"))
```

Signed and encrypted values are never interchangeable: each uses its own key, derived from `APP_KEY`.

:::warning
A signed value is readable by anyone who sees it, only encoded. Never sign data that must stay secret, such as an email address in a public link: encrypt it instead.

```go del={1} ins={2}
	token, err := enc.Sign(Invite{Email: email, TeamID: team.ID})
	token, err := enc.Encrypt(Invite{Email: email, TeamID: team.ID})
```
:::

## Find encrypted values

Encrypted values can't be searched: the same email address encrypts differently every time, so `WHERE email = ?` never matches. A **blind index** solves this. It's a keyed hash of the value, stored in its own column and used only to look rows up. Equal values give equal indexes, but the index reveals nothing about the value without the key.

The purpose names the column, so the same value gives different indexes in different columns, and they can't be compared:

```go title="users.go"
	email = strings.ToLower(strings.TrimSpace(email)) // indexes match exact values
	encrypted, err := enc.Encrypt(email)
	if err != nil {
		return err
	}
	_, err = db.ExecContext(ctx,
		"INSERT INTO users (email, email_index) VALUES ($1, $2)",
		encrypted, enc.BlindIndex(email, "users.email"))
```

```go title="users.go"
	row := db.QueryRowContext(ctx,
		"SELECT id, email FROM users WHERE email_index = $1",
		enc.BlindIndex(strings.ToLower(strings.TrimSpace(email)), "users.email"))
```

Normalize a value before indexing it, as above: `Ada@example.com` and `ada@example.com` give different indexes.

## Rotate the App's key

Pass several keys to `New`, the newest first. The first key encrypts and signs every new value, and every key decrypts and verifies, so values made with an older key keep working while you replace it:

```go title="main.go"
	enc, err := encryption.New(e.Secret("APP_KEY"), e.Secret("OLD_APP_KEY"))
```

To rotate the key:

1. Generate a new key with `act generate:key --show`.
2. Set `APP_KEY` to the new key, and `OLD_APP_KEY` to the previous one.
3. Deploy.
4. Once the values made with the old key have expired or been encrypted again, remove `OLD_APP_KEY`.

Blind indexes depend on the key too. While rotating, look rows up with `BlindIndexes`, which returns the index under every key, and write new rows with `BlindIndex`, which uses the newest:

```go
	indexes := enc.BlindIndexes(email, "users.email")
	rows, err := db.QueryContext(ctx, "SELECT id FROM users WHERE email_index = ANY($1)", indexes)
```

:::warning
Never remove an old key while values made with it are still in use. They become unreadable for good: encrypted columns can't be decrypted, and signed links stop working.
:::

See also: [Response](../basics/response.md#set-cookies) for signed and encrypted cookies, [URL builder](../basics/url-builder.md#signed-urls) for signed URLs, [Configuration & Environment](../../configuration.md)
