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

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 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, the receiver (non-sender) creates a PAKE instance with role 0:

// 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:

// 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. The message contains the raw PAKE bytes and the curve specification:

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 and updates its local PAKE instance with the receiver's public value:

// 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:

// 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 package provides Encrypt and Decrypt functions using AES-GCM. When transmitting file metadata or chunks, src/message/message.go encodes messages using the derived key:

// 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 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 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, 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →