I’ve been dealing a lot with the age encryption protocol lately, and had a rough idea of how the scheme worked, but I finally wanted to sit down and work it out until it actually made sense.

As background, we have two parties, a sender, someone who wants to encrypt and send a file. We denote that party as U. Second, we have the recipient, that will receive that file and be able to decrypt it. We denote that party as V. The file should have a per-file unique encryption key, and we need some way of getting that key from U to V without having to have already exchanged that key, or, indeed, having any secret information shared between the two parties.

Age, an many other schemes with similar goals, achieves this by relying on *public key
cryptography*. Each party has a private key, which they keep secret. That private key
has an associated public key, which they can distribute freely without compromising
the value of the private key. Age in particular uses
elliptic curve cryptography,
a set of mathematical operations over a finite field known as an elliptic curve, the mathematics
of which provides some useful properties which can be turned into encryption primatives.
I won’t go into the details of those properties, but do encourage you to check out
the many resources online, including this Computerphile video.

We’re going to work through the mechanics of how age builds up a shared secret between
U and V, without either of those parties having previously exchanged any secret
information. I’m going to do that know with some annotated code, in particular, the
file `age/internal/age/x25519.go`

. All of the code here is from that file as it appears
in commit `f0f8092d60`

.

Let’s start on the sender’s side, U. Wrapping the per-file encryption key is done in this function

`func (r *X25519Recipient) Wrap(fileKey []byte) (*format.Recipient, error)`

First we make an ephemeral secret by reading some random bits in:

```
ephemeral := make([]byte, curve25519.ScalarSize)
if _, err := rand.Read(ephemeral); err != nil {
return nil, err
}
```

In crypto terms, this is dU, and that’s all a private key is: simply enough random bits to fit the elliptic curve size — here, a specific curve called https://en.wikipedia.org/wiki/Curve25519.

Next, figure out the public side of the `ephemeral`

key:

```
ourPublicKey, err := curve25519.X25519(ephemeral, curve25519.Basepoint)
```

This is performing a scalar multiplication against the Curve25519 basepoint
(sometimes called a *generator*, G). The basepoint is simply one of the
constants that defines this particular elliptic curve. In mathematical
terms, we now have the sender’s public key, dUG.

Now we create the shared secret:

```
sharedSecret, err := curve25519.X25519(ephemeral, r.theirPublicKey)
```

`r`

in this case is the recipient, so `r.theirPublicKey`

is, in our terms,
dVG, and the value of the shared secret is dUdVG.

The shared secret is good as that, but not as a key. A good key should be
something that basically looks like a random number, but we know that
dUdVG is some point that lives on the curve G. It needs to be
*stretched* into a good key. The method age takes is a venerable one, using
an HKDF, a hashed key derivation formula:

```
salt := make([]byte, 0, len(ourPublicKey)+len(r.theirPublicKey))
salt = append(salt, ourPublicKey...)
salt = append(salt, r.theirPublicKey...)
h := hkdf.New(sha256.New, sharedSecret, salt, []byte(x25519Label))
wrappingKey := make([]byte, chacha20poly1305.KeySize)
```

`salt`

and `x25519Label`

are things there to tie this key to a specific context.
The salt is both public keys concatenated together, and the `x25519Label`

is just
a string “age-encryption.org/v1/X25519”. The function of all of this is to turn
the shared secret into something functionally indistinguishable from random noise.
Primarily, by going through this we no longer guarantee that `wrappingKey`

is a
point on Curve25519.

Finally, we use `wrappingKey`

as a symmetric key to encrypt the per-file key:
`wrappedKey, err := aeadEncrypt(wrappingKey, fileKey)`

.

Now let’s look at the recipient V side, handled in

```
func (i *X25519Identity) Unwrap(block *format.Recipient) ([]byte, error)
```

After some sanity checking, we extract the sender’s public key, which was sent along with the encrypted file:

```
publicKey, err := format.DecodeString(block.Args[0])
```

This gives us dUG, by definition. V already knows its secret key, which is dV. Now we use that information to derive the shared secret:

```
sharedSecret, err := curve25519.X25519(i.secretKey, publicKey)
```

This is dVdUG, which, because multiplication is associative on this curve, can
be re-arranged into dUdVG, which is the exact same `sharedSecret`

that U used. Do the same `HKDF`

key derivation (V knows both public
keys and `x25519Label`

is a constant) and we get the same `wrappingKey`

and
can now decrypt the `fileKey`

and use that to decrypt the file.

And that’s it.