How croc's End-to-End Encryption Works with PAKE: A Deep Dive into the Handshake
croc secures file transfers using Password-Authenticated Key Exchange (PAKE) to transform a short human-readable code into a strong symmetric session key that encrypts all data via AES-GCM.
The open-source tool schollz/croc implements a zero-configuration file transfer system where security relies on a PAKE handshake that occurs before any payload transmission. This mechanism ensures that even if an attacker intercepts the network traffic, they cannot brute-force the shared secret or decrypt the transferred files without participating in the live exchange.
The PAKE Handshake Architecture
The PAKE exchange in croc happens over the control channel immediately after the TCP connection establishes. Both parties—the sender and the receiver—initialize separate PAKE instances using the shared secret (the transfer "code") and exchange public components to derive an identical session key.
Initializing the Exchange with Distinct Roles
In src/croc/croc.go, the handshake begins when both sides create PAKE instances through the external github.com/schollz/pake/v3 library. The receiver (role 0) and sender (role 1) use different role identifiers to ensure cryptographic separation:
// Receiver initialization (croc.go L288-L291)
c.Pake, err = pake.InitCurve([]byte(c.Options.SharedSecret[5:]), 0, c.Options.Curve)
// Sender initialization (croc.go L1150-L1154)
c.Pake, err = pake.InitCurve([]byte(c.Options.SharedSecret[5:]), 1, c.Options.Curve)
The InitCurve function takes three parameters: the shared secret bytes (trimmed to remove the first five characters), the role integer, and the elliptic curve name (defaulting to p256).
Exchanging Public Components
After initialization, the receiver transmits its public PAKE value through the unencrypted control channel using a message of type TypePAKE. This occurs in src/croc/croc.go at lines 1796-1802:
message.Send(c.conn[0], c.Key, message.Message{
Type: message.TypePAKE,
Bytes: c.Pake.Bytes(),
Bytes2: []byte(c.Options.Curve),
})
The sender receives this payload and updates its own PAKE instance with the receiver's public value (lines 2094-2105):
c.Pake, err = pake.InitCurve([]byte(c.Options.SharedSecret[5:]), 1, string(m.Bytes2))
// ...
err = c.Pake.Update(m.Bytes)
Deriving the Shared Session Key
Once both parties have exchanged public components, each calls SessionKey() to compute the identical symmetric key. This 32-byte secret becomes the foundation for all subsequent encryption:
// Both sides execute this (croc.go L1209-L1212)
sessionKey, err := c.Pake.SessionKey()
c.Key = sessionKey
After this step, c.Key is stored in the client instance and passed to all future message.Send and message.Decode calls, ensuring that file metadata and payload chunks travel encrypted.
Cryptographic Implementation Details
Curve Selection and Parameters
The c.Options.Curve field determines which elliptic curve performs the underlying Diffie-Hellman operations. By default, croc uses the NIST P-256 curve, though the pake library supports multiple curves via pake.AvailableCurves(). The curve name travels alongside the PAKE payload in Bytes2, ensuring both parties negotiate identical cryptographic parameters even if defaults differ.
From PAKE to AES-GCM Encryption
Once the session key is established, croc delegates encryption to the internal crypt package (src/crypt/crypt.go). The message package (src/message/message.go, lines 40-63) acts as the intermediary:
// Encryption before transmission
encryptedPayload, err := crypt.Encode(bytes, key)
// Decryption upon receipt
decryptedPayload, err := crypt.Decode(raw, key)
The crypt implementation uses AES-GCM with a random nonce generated for each message. The nonce prepends the ciphertext, allowing the receiver to decrypt without additional out-of-band data. This design provides authenticated encryption, detecting any tampering with transmitted chunks.
Error Handling and Security Guarantees
If any stage of the PAKE handshake fails—whether through curve mismatch, secret disagreement, or network corruption—croc aborts the transfer immediately. The error handling at lines 314-322 in croc.go wraps these failures as pakeHandshakeError, presenting users with a clear "pake not successful" message rather than cryptic cryptographic failures.
This architecture provides three critical security properties:
- Password Authentication: The low-entropy "code" authenticates both parties without exposing the secret to offline dictionary attacks, thanks to PAKE's interactive nature.
- Forward Secrecy: Each transfer generates a fresh session key; compromising the shared secret after transmission completion does not expose historical file contents.
- No Pre-Shared Keys: Users need only agree on the human-readable transfer code, while the heavy cryptographic lifting (ECDH key agreement) happens automatically.
Code Walkthrough: The Complete Handshake
Below is a consolidated view of how croc implements the PAKE handshake in practice, combining logic from src/croc/croc.go and src/message/message.go:
// Receiver side (role 0)
receiverPake, _ := pake.InitCurve([]byte(secret[5:]), 0, "p256")
msg := message.Message{
Type: message.TypePAKE,
Bytes: receiverPake.Bytes(),
Bytes2: []byte("p256"),
}
message.Send(conn, nil, msg) // Sent without encryption (key is nil)
// Sender side (role 1)
senderPake, _ := pake.InitCurve([]byte(secret[5:]), 1, "p256")
// Receive the PAKE payload
var incoming message.Message
message.Decode(nil, rawBytes, &incoming)
senderPake.Update(incoming.Bytes)
sessionKey, _ := senderPake.SessionKey()
// All subsequent messages use the derived key
fileMsg := message.Message{Type: message.TypeFileInfo, Bytes: fileData}
message.Send(conn, sessionKey, fileMsg) // AES-GCM encrypted
Summary
- croc uses the external
github.com/schollz/pake/v3library to perform Password-Authenticated Key Exchange before transmitting any file data. - Role differentiation (receiver as
0, sender as1) ensures cryptographic separation during thepake.InitCurveinitialization insrc/croc/croc.go. - Public component exchange happens via unencrypted
TypePAKEmessages that carry the curve name and PAKE bytes. - Session key derivation occurs through
SessionKey(), producing a 32-byte symmetric key stored inc.Keyfor the transfer duration. - AES-GCM encryption protects all subsequent communication through the
cryptpackage, using the PAKE-derived key for both file metadata and payload chunks. - Failure handling immediately aborts transfers if the handshake fails, preventing silent security degradation.
Frequently Asked Questions
What is PAKE and why does croc use it?
PAKE (Password-Authenticated Key Exchange) is a cryptographic protocol that allows two parties to establish a shared secret key using a low-entropy password, without exposing the password to offline brute-force attacks. croc uses PAKE because it allows users to authenticate transfers using simple, human-readable codes (like "croc1234") while still achieving strong encryption equivalent to high-entropy keys. The protocol ensures that an eavesdropper observing the network traffic gains no advantage in guessing the shared secret.
How does croc prevent man-in-the-middle attacks during the PAKE handshake?
croc mitigates man-in-the-middle attacks through the interactive nature of PAKE combined with the shared secret requirement. Since both parties must possess the identical transfer code to derive the same session key, an attacker who intercepts the connection cannot establish a valid encrypted channel without knowing the code. Additionally, if the PAKE exchange produces mismatched keys (detected when subsequent encrypted messages fail to decrypt), the transfer aborts immediately with a handshake error.
What encryption algorithm protects the actual file data after the PAKE handshake completes?
After PAKE derives the session key, croc uses AES-GCM (Galois/Counter Mode) to encrypt all file data and control messages. The implementation in src/crypt/crypt.go creates a new AES cipher from the 32-byte PAKE session key, wraps it in GCM mode for authenticated encryption, and generates a random nonce for each message. This ensures both confidentiality and integrity for every chunk of transferred data.
Can I use a different elliptic curve for the PAKE exchange in croc?
Yes, croc supports multiple elliptic curves through the pake library's AvailableCurves() function, configurable via the --curve flag. While the default is p256 (NIST P-256), users can specify alternative curves during client initialization. The chosen curve name travels in the Bytes2 field of the initial PAKE message, ensuring both sender and receiver negotiate the same curve parameters regardless of local defaults.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →