# How croc's PAKE-Based End-to-End Encryption Works: Secure File Transfer Handshake Explained

> Understand croc's PAKE-based end-to-end encryption. Learn how croc establishes a secure channel using a human-readable code for protected file transfers before sending data.

- Repository: [Zack/croc](https://github.com/schollz/croc)
- Tags: deep-dive
- Published: 2026-07-30

---

**croc derives a symmetric encryption key from a short human-readable code using Password-Authenticated Key Exchange (PAKE), establishing an authenticated secure channel before any file payload is transmitted.**

The **schollz/croc** file transfer tool implements **PAKE-based end-to-end encryption** to protect data in transit without requiring pre-shared high-entropy keys. By leveraging the external `github.com/schollz/pake/v3` library, croc converts the low-entropy "code" displayed to users into a strong 32-byte symmetric key through an authenticated Diffie-Hellman exchange. This process occurs entirely within the control channel logic of [`src/croc/croc.go`](https://github.com/schollz/croc/blob/main/src/croc/croc.go) before any file bytes are transmitted.

## PAKE Initialization and Roles

The handshake begins with both parties initializing curve-based PAKE instances using the shared secret and distinct role identifiers. In [`src/croc/croc.go`](https://github.com/schollz/croc/blob/main/src/croc/croc.go), the receiver (non-sender) creates a PAKE instance with role `0`:

```go
// Receiver initialization around line 288
c.Pake, err = pake.InitCurve([]byte(c.Options.SharedSecret[5:]), 0, c.Options.Curve)

```

Simultaneously, the sender initializes its instance with role `1` before processing the incoming PAKE payload:

```go
// Sender initialization around line 1150
pakeS, err := pake.InitCurve([]byte(c.Options.SharedSecret[5:]), 1, c.Options.Curve)

```

The `InitCurve` function takes the password material (the transfer code), a role identifier (0 for receiver, 1 for sender), and an elliptic curve name (defaulting to `p256`).

## The PAKE Message Exchange

After initialization, the receiver transmits its public PAKE component over the control channel via an unencrypted `TypePAKE` message defined in [`src/message/message.go`](https://github.com/schollz/croc/blob/main/src/message/message.go). The message contains the raw PAKE bytes and the curve specification:

```go
msg := message.Message{
    Type:   message.TypePAKE,
    Bytes:  c.Pake.Bytes(),
    Bytes2: []byte(c.Options.Curve),
}
message.Send(c.conn[0], nil, msg) // nil key indicates unencrypted PAKE exchange

```

The sender receives this payload in [`src/croc/croc.go`](https://github.com/schollz/croc/blob/main/src/croc/croc.go) and updates its local PAKE instance with the receiver's public value:

```go
// Processing incoming PAKE around line 2094
err = pakeS.Update(m.Bytes)

```

This `Update` call performs the cryptographic computation that allows both parties to arrive at the identical shared secret while cryptographically proving possession of the original password.

## Session Key Derivation and Storage

Once the public values are exchanged, both sides derive the symmetric session key by calling `SessionKey()`. The implementation stores this key in the connection struct for subsequent encryption operations:

```go
// Key derivation around line 1209
sessionKey, err := c.Pake.SessionKey()
c.Key = sessionKey

```

This 32-byte key becomes the foundation for all subsequent encryption. The PAKE protocol ensures that the resulting key is cryptographically strong even though the original shared secret was short and human-readable, and it prevents offline brute-force attacks against the password material.

## Encrypting the Data Channel with AES-GCM

With the session key established in `c.Key`, croc transitions to symmetric encryption for all remaining communication. The [`src/crypt/crypt.go`](https://github.com/schollz/croc/blob/main/src/crypt/crypt.go) package provides `Encrypt` and `Decrypt` functions using AES-GCM. When transmitting file metadata or chunks, [`src/message/message.go`](https://github.com/schollz/croc/blob/main/src/message/message.go) encodes messages using the derived key:

```go
// Subsequent encrypted messages
msg := message.Message{
    Type: message.TypeFileInfo,
    Bytes: fileInfoJSON,
}
message.Send(conn, sessionKey, msg) // Encrypts via crypt.Encrypt

```

The `Encode` function internally calls `crypt.Encrypt`, which generates a random nonce, encrypts the payload using `cipher.NewGCM(aes.NewCipher(key))`, and prepends the nonce to the ciphertext. The receiver uses `message.Decode` with the same key to decrypt and verify the authenticity of incoming messages.

## Curve Selection and Security Properties

By default, croc uses the **P-256** elliptic curve for the PAKE exchange, specified via `c.Options.Curve` when calling `InitCurve`. Each transfer generates independent ephemeral key pairs, providing **forward secrecy**—compromise of a single session key does not affect past or future transfers. The protocol binds the session key generation to successful mutual authentication, ensuring that an attacker cannot verify password guesses without interacting with a legitimate party.

## Summary

- **PAKE Initialization**: Both sender and receiver call `pake.InitCurve` in [`src/croc/croc.go`](https://github.com/schollz/croc/blob/main/src/croc/croc.go) with the shared secret and distinct roles (0 and 1) to generate ephemeral key pairs.
- **Public Exchange**: The receiver sends its PAKE public component via an unencrypted `TypePAKE` message; the sender updates its state with this value using `Update()`.
- **Key Derivation**: Both parties call `SessionKey()` to derive identical 32-byte symmetric keys that authenticate both ends while protecting the password from offline attacks.
- **Channel Encryption**: All subsequent messages use AES-GCM encryption via `crypt.Encrypt` and `crypt.Decrypt` in the `crypt` package, using the PAKE-derived key stored in `c.Key`.
- **Security Model**: The implementation provides forward secrecy and resistance to brute-force attacks on the low-entropy transfer code.

## Frequently Asked Questions

### What cryptographic curve does croc use for PAKE?

croc defaults to the **P-256** elliptic curve, specified in [`src/croc/croc.go`](https://github.com/schollz/croc/blob/main/src/croc/croc.go) via the `Curve` option passed to `pake.InitCurve`. This curve provides 128-bit security while maintaining efficient computation across devices. The underlying `github.com/schollz/pake/v3` library supports multiple curves, but croc typically uses P-256 for broad compatibility.

### How does PAKE protect against brute-force attacks on the transfer code?

PAKE protocols are specifically designed to be **immune to offline dictionary attacks**. An attacker intercepting the PAKE handshake cannot verify password guesses without interacting with the legitimate parties, and both sides automatically reject connections after failed authentication. This means the short human-readable codes remain secure despite their low entropy.

### What happens if the sender and receiver enter different codes?

If the shared secrets do not match, the `Update` and `SessionKey` operations in the PAKE protocol will fail to converge on the same key. croc detects this mismatch during the handshake phase in [`src/croc/croc.go`](https://github.com/schollz/croc/blob/main/src/croc/croc.go), aborting the connection with a "pake not successful" error before any file payload is transmitted.

### Is the PAKE exchange itself encrypted?

**No**, the initial PAKE messages are sent unencrypted over the control channel. This is safe because PAKE protocols are designed to withstand eavesdropping—observing the public values exchanged during `TypePAKE` messages does not allow an attacker to derive the session key or verify password guesses offline. Only after `SessionKey()` is successfully called does croc enable AES-GCM encryption for all subsequent communication.