croc Protocol Specification: Technical Deep Dive into Secure File Transfer

The croc protocol specification defines a lightweight, end-to-end encrypted file transfer protocol built on TCP that uses PAKE authentication and JSON-encoded messages to securely exchange files between peers without exposing encryption keys to intermediate relays.

The croc protocol powers the popular command-line tool developed by schollz/croc, enabling secure cross-platform file transfers without port forwarding or centralized storage. This protocol specification outlines how the system establishes encrypted connections, negotiates keys, and streams file data through relays while maintaining zero-knowledge privacy guarantees.

Protocol Architecture Overview

The croc protocol operates over a simple TCP relay architecture and consists of six distinct phases. Each phase builds upon the previous to create a secure, reliable transfer channel:

  • Connection Handshake – Protocol version validation
  • PAKE Key Exchange – Password-authenticated secret derivation
  • Secure Channel Establishment – Encryption context initialization
  • Metadata Exchange – File information negotiation
  • Data Transfer – Chunked file transmission with flow control
  • Completion – Graceful connection termination

Connection and Handshake

When two peers connect to a relay, they immediately exchange a fixed validation payload to confirm protocol compatibility. According to the source code in src/croc/croc.go, the handshake transmits the byte slice []byte("handshake") via the handshakeRequest mechanism.

This simple greeting serves two purposes: it confirms the remote peer is running croc-compatible software, and it validates that the TCP connection is stable before cryptographic operations begin. If the handshake fails or receives unexpected bytes, the connection aborts immediately.

PAKE Key Exchange

After the handshake completes, peers initiate a Password Authenticated Key Exchange (PAKE) using the schollz/pake library. This phase utilizes the TypePAKE message constant defined in src/message/message.go.

The PAKE algorithm allows both parties to derive an identical shared secret (c.Key) using only a pre-shared passphrase (the "code" in croc terminology). Critically, the protocol never transmits the passphrase or the derived key across the wire, preventing relay operators from intercepting encryption material even if they control the network path.

Message Structure and Encoding

All protocol traffic travels inside the Message struct defined in src/message/message.go. The structure uses JSON tags for serialization and supports both control messages and binary payloads:

type Message struct {
    Type    Type   `json:"t,omitempty"` // Message type constant
    Message string `json:"m,omitempty"` // Optional text payload
    Bytes   []byte `json:"b,omitempty"` // Primary binary payload
    Bytes2  []byte `json:"b2,omitempty"`// Secondary payload (PAKE data)
    Num     int    `json:"n,omitempty"` // Numeric field (chunk indices)
}

The encoding pipeline follows a strict sequence in the Encode function. First, the struct undergoes JSON marshaling. Next, compress.Compress applies fastlz compression to the JSON bytes. Finally, if a PAKE-derived key exists (c.Key != nil), crypt.Encrypt encrypts the compressed payload using AES-GCM; otherwise, the data transmits in cleartext. The decoding process reverses these steps.

Metadata Exchange

Before transferring file contents, the sender transmits file descriptors using TypeFileInfo messages. These payloads contain JSON-encoded metadata including filename, size, and hash values. The recipient acknowledges readiness using TypeRecipientReady messages.

This negotiation allows the receiver to validate available disk space and verify file integrity before accepting potentially gigabytes of data. The metadata exchange occurs over the encrypted channel established during PAKE, ensuring that file names and sizes remain confidential from the relay.

Data Transfer and Flow Control

File contents flow as a sequence of chunks, with each chunk wrapped in a Message carrying the payload bytes and offset index. The protocol references chunk handling through the Chunk structure in src/croc/croc.go.

Rather than naive streaming, croc implements sliding-window flow control using golang.org/x/time/rate to prevent overwhelming slow receivers. Each chunk receives independent sequence numbering via the Num field, enabling out-of-order delivery handling and progress tracking during intermittent network conditions.

Session Termination

Upon completing all chunk transmissions, the sender broadcasts a TypeFinished message defined in src/message/message.go. Both parties then exchange close-sender and close-recipient messages before closing underlying TCP streams.

This graceful shutdown ensures that buffers flush completely and that both sides agree on transfer completion status before releasing resources. Unfinished transfers can resume from the last acknowledged chunk index due to the stateful nature of the protocol.

Security Guarantees

The croc protocol specification provides three fundamental security properties:

  • End-to-end encryption – The relay never learns the encryption key because PAKE exchange occurs directly between peers, and all subsequent traffic uses AES-GCM encryption implemented in src/crypt/crypt.go.
  • Forward secrecy – Each transfer generates a fresh PAKE exchange, producing a unique session key that cryptographically isolates individual transfer sessions.
  • Integrity verification – The encryption layer includes HMAC authentication, preventing tampering with transferred data even if an attacker controls the relay infrastructure.

Implementation Example

The following Go code demonstrates the protocol flow for custom implementations:

// 1. Create a client (sender or receiver)
c := croc.NewClient(croc.Options{
    IsSender:     true,
    SharedSecret: "my-secret-phrase",
    RoomName:     "myroom",
})

// 2. Perform handshake and PAKE exchange automatically
if err := c.Start(); err != nil {
    log.Fatal(err)
}

// 3. Send file metadata
msg := message.Message{
    Type:  message.TypeFileInfo,
    Bytes: []byte(`{"n":"example.txt","s":12345}`),
}
message.Send(c.conn[0], c.Key, msg)

// 4. Stream data chunks
chunk := make([]byte, 64*1024)
n, _ := file.Read(chunk)
msg = message.Message{
    Type:  message.Type("data"),
    Bytes: chunk[:n],
    Num:   chunkIndex,
}
message.Send(c.conn[0], c.Key, msg)

// 5. Signal completion
message.Send(c.conn[0], c.Key, message.Message{Type: message.TypeFinished})

For standard usage, the CLI abstracts these details:


# Sender

croc send --code "my-secret-phrase" bigfile.zip

# Receiver

croc receive --code "my-secret-phrase"

Summary

  • The croc protocol specification defines a six-phase handshake that combines TCP reliability with PAKE-based key agreement.
  • All messages use a standardized JSON struct that undergoes compression (compress.Compress) and conditional encryption (crypt.Encrypt) based on the derived session key.
  • File transfers occur via sequenced chunks with sliding-window flow control to manage network congestion.
  • End-to-end encryption ensures relays operate as blind data conduits, unable to decrypt or modify transferred content.
  • Key implementation files include src/croc/croc.go for state management, src/message/message.go for encoding, and src/crypt/crypt.go for cryptographic primitives.

Frequently Asked Questions

What encryption algorithm does the croc protocol use?

The croc protocol uses AES-GCM for symmetric encryption of message payloads, implemented in src/crypt/crypt.go. The encryption keys derive from a PAKE (Password Authenticated Key Exchange) operation that establishes a shared secret without transmitting it over the network, ensuring that only the two endpoints can decrypt the traffic.

How does croc ensure the relay cannot access transferred files?

The protocol achieves zero-knowledge relay operation through end-to-end encryption. Because the PAKE exchange happens directly between peers and generates a session key never shared with the relay, the relay only sees encrypted byte streams. The relay cannot decrypt metadata (filenames, sizes) or file contents because it lacks the cryptographic key material generated during the handshake phase.

What is the maximum file size supported by the croc protocol?

The protocol itself imposes no theoretical file size limits; it transfers files as a sequence of chunks referenced by integer indices in the Num field of the Message struct. Practical limits depend on available disk space and the maximum value of Go's int type on the target platform (typically 9 exabytes on 64-bit systems). The chunked approach allows resumption of interrupted transfers regardless of file size.

Is the croc protocol specification open for third-party implementations?

Yes, the protocol is fully open and documented in the reference implementation at schollz/croc. Third-party implementations must adhere to the message format defined in src/message/message.go, implement the PAKE handshake using compatible parameters, and follow the encoding sequence of JSON marshaling, fastlz compression, and AES-GCM encryption. The fixed handshake byte string []byte("handshake") serves as the protocol version identifier for compatibility checking.

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 →