# `Capstan.Protocol.Handshake`
[🔗](https://github.com/baselabs/capstan/blob/v1.3.1/lib/capstan/protocol/handshake.ex#L1)

MySQL connection handshake, authentication and TLS upgrade.

A pure protocol module: it takes an already-connected `{:gen_tcp, socket}` plus
resolved options and drives, in order, the initial-handshake parse, an optional
TLS upgrade, and authentication — returning an authenticated, transport-tagged
socket (`{:gen_tcp, _}` or `{:ssl, _}`, per `Capstan.Protocol.Packet`) and the
negotiated server capabilities. It owns no process, no config validation and no
socket lifecycle; `Capstan.Connection` supplies the socket and the options.

## Transport posture (design Q6 / Q17)

TLS is expected to be on by default (the caller passes `ssl: false` to opt out).
Peer verification is an **explicit operator choice, never a silent default**: with
`ssl: true` the caller must supply EITHER a `cacertfile`/`cacerts` OR an explicit
`verify:` in `ssl_opts`. Given neither, `connect/2` fails closed with
`:tls_verification_unspecified` rather than silently selecting `verify_none` —
OTP would otherwise default to `verify_peer`, which cannot validate MySQL's
self-signed auto-generated certificate, and the tempting "fix" is unauthenticated
TLS chosen by nobody.

## Authentication (design Q6)

`caching_sha2_password` is the preferred plugin, including its RSA-public-key
full-auth path. `mysql_native_password` is honoured only when named in
`auth_plugins`; using it logs a deprecation warning (removed in MySQL 9.x).

The full-auth secret handling is fail-closed: on a full-auth request over TLS the
protocol permits the secure-channel fast path (the password crosses the already
encrypted channel). Over a plaintext channel the password is **never** sent in
the clear — it is XORed with the auth nonce and RSA-OAEP encrypted with the
server's public key; if that key cannot be obtained, `connect/2` fails closed
with `:insecure_auth_refused`, and a server-supplied key that does not decode
fails closed with `:bad_public_key` (never a raise).

## Rule 1

The connection password appears in no returned term, error, or log line emitted
by this module.

# `connect_opt`

```elixir
@type connect_opt() ::
  {:username, String.t()}
  | {:password, String.t()}
  | {:database, String.t() | nil}
  | {:ssl, boolean()}
  | {:ssl_opts, keyword()}
  | {:auth_plugins, [atom()]}
  | {:host, charlist() | String.t()}
  | {:timeout, timeout()}
```

Resolved connection options.

# `initial_handshake`

```elixir
@type initial_handshake() :: %{
  server_version: String.t(),
  connection_id: non_neg_integer(),
  salt: binary(),
  auth_plugin: String.t(),
  capabilities: non_neg_integer(),
  charset: byte(),
  status: non_neg_integer()
}
```

Parsed `Protocol::HandshakeV10` initial handshake.

# `result`

```elixir
@type result() :: %{
  socket: socket(),
  server_version: String.t(),
  capabilities: non_neg_integer(),
  connection_id: non_neg_integer(),
  tls: boolean()
}
```

Result of a successful handshake.

# `socket`

```elixir
@type socket() :: Capstan.Protocol.Packet.socket()
```

A transport-tagged socket, as defined by `Capstan.Protocol.Packet`.

# `caching_sha2_scramble`

```elixir
@spec caching_sha2_scramble(binary(), binary()) :: binary()
```

The `caching_sha2_password` fast-auth scramble.

`SHA256(password) XOR SHA256(SHA256(SHA256(password)) <> nonce)`. An empty
password scrambles to an empty response.

# `connect`

```elixir
@spec connect(socket(), [connect_opt()]) :: {:ok, result()} | {:error, term()}
```

Reads the initial handshake, optionally upgrades to TLS, and authenticates.

`socket` is an already-connected `{:gen_tcp, socket}`. On success returns
`{:ok, result}` carrying the authenticated (possibly TLS-upgraded) socket and the
negotiated capabilities; on failure returns `{:error, reason}` with a value-free
reason.

# `encrypt_password`

```elixir
@spec encrypt_password(binary(), binary(), binary()) ::
  {:ok, binary()} | {:error, :bad_public_key}
```

Encrypts the password for the `caching_sha2_password` full-auth path.

The NUL-terminated password is XORed with the auth nonce (cycled) and then
RSA-OAEP encrypted with the server's PEM-encoded public key. A public key that
does not decode fails closed with `{:error, :bad_public_key}` — the password is
never sent when the key is unusable.

# `native_scramble`

```elixir
@spec native_scramble(binary(), binary()) :: binary()
```

The `mysql_native_password` scramble (opt-in plugin).

`SHA1(password) XOR SHA1(nonce <> SHA1(SHA1(password)))`. An empty password
scrambles to an empty response.

# `parse_initial_handshake`

```elixir
@spec parse_initial_handshake(binary()) ::
  {:ok, initial_handshake()} | {:error, term()}
```

Parses a `Protocol::HandshakeV10` initial handshake packet.

Returns `{:ok, handshake}` or `{:error, reason}` (a server error packet becomes
`{:error, {:handshake_error, code}}`).

---

*Consult [api-reference.md](api-reference.md) for complete listing*
