How Encryption Key Management Works in OpenFlux: Runtime Injection and scrypt Derivation

OpenFlux externalizes encryption keys through file-based runtime injection, deriving AES-256-GCM directional keys via scrypt KDF rather than embedding secrets in source code.

OpenFlux is an open-source transport layer tool that secures communications between clients and exit nodes. Unlike systems that compile cryptographic material into binaries, OpenFlux implements strict separation between code and secrets, requiring operators to supply encryption keys at runtime through external files. This architecture ensures that the only secret traversing the network is ciphertext, while the clear-text key never leaves the host filesystem.

Runtime Key Injection via Command-Line Flags

OpenFlux refuses to embed encryption keys in its source code. Instead, operators must provide the shared secret through a file path specified by the --encryptionKeyFile flag.

Reading the Secret from Disk

Early in main/main.go (lines 84–86), the program reads the raw bytes from the specified file, trims whitespace, and passes the result to the transport layer:

secretBytes, err := os.ReadFile(*encryptionKeyFile)
...
encrypted, err := transport.NewEncryptedTransport(
    inner,
    strings.TrimSpace(string(secretBytes)),
    context,
    *exitNode,
)

The secret is treated as a shared password rather than a raw encryption key, triggering a rigorous key derivation process before any data encryption occurs.

Cryptographic Key Derivation Architecture

Once injected, the shared secret undergoes a multi-stage transformation in transport/encrypted.go to generate cryptographically independent directional keys.

Master Key Generation with scrypt

The NewEncryptedTransport function treats the user-supplied secret as input to a scrypt-based Key Derivation Function (KDF). The implementation (lines 58–62) uses the following parameters:

  • N (cost factor): 32768
  • r (block size): 8
  • p (parallelization): 1
  • Output length: 32 bytes

The salt is deterministically generated via SHA-256:

salt := sha256.Sum256([]byte("OpenFlux encrypted transport v1\x00" + context))
master, err := scrypt.Key([]byte(secret), salt[:], 32768, 8, 1, 32)

The context parameter—typically the transport type or document URL—ensures domain separation between different communication channels.

Directional Key Separation

To prevent key reuse attacks, OpenFlux derives two independent keys from the master key using HMAC-SHA256 (lines 63–64):

clientToExit := deriveDirectionalKey(master, "client-to-exit")
exitToClient := deriveDirectionalKey(master, "exit-to-client")

This bidirectional isolation ensures that compromising traffic in one direction does not expose the reverse channel.

AES-256-GCM Transport Implementation

Each directional key initializes an AES-256-GCM AEAD (Authenticated Encryption with Associated Data) construct within the EncryptedTransport struct. This provides both confidentiality and integrity for every packet traversing the network.

Replay Protection and Nonce Management

The transport layer maintains a sliding window that tracks the last 4096 nonces. Any packet containing a duplicate or stale nonce is automatically rejected, preventing replay attacks against the encrypted channel.

Security Validation and Constraints

OpenFlux enforces strict safety checks before accepting encryption material. The NewEncryptedTransport function validates that secrets contain at least 16 characters, aborting initialization with an explicit error if shorter strings are supplied:

if len(secret) < 16 {
    return nil, errors.New("encryption secret must contain at least 16 characters")
}

This minimum length requirement mitigates brute-force attacks against the scrypt-based KDF.

Practical Implementation Examples

Running OpenFlux with External Key Files

Create a strong secret and launch the client:


# Create a strong secret (at least 16 characters)

echo "my-super-secret-password-1234" > /path/to/flux.key

# Start the client with encryption enabled

./openflux \
    --encryptionKeyFile=/path/to/flux.key \
    --transport=websocket \
    --docUrl=https://example.com/mydoc \
    --socksAddr=:1080

Programmatic Transport Creation

For testing or embedded Go applications:

secret := []byte("a sufficiently long shared secret")
secretFile := "/tmp/flux.key"
os.WriteFile(secretFile, secret, 0600)

inner := transport.NewRawSocketTransport()
enc, err := transport.NewEncryptedTransport(
    inner,
    strings.TrimSpace(string(secret)),
    "example.com/doc",
    false,
)
if err != nil {
    log.Fatalf("cannot create encrypted transport: %v", err)
}

Generating Cryptographically Secure Keys

Use system entropy for production deployments:

head -c 32 /dev/urandom | base64 > /secure/location/flux.key
chmod 600 /secure/location/flux.key

Summary

  • Externalized secrets: Encryption keys are read at runtime via --encryptionKeyFile, never embedded in the p1neappleXpress/OpenFlux source code.
  • Memory-hard KDF: scrypt with parameters (32768, 8, 1) transforms short passwords into 32-byte master keys resistant to hardware-accelerated attacks.
  • Domain separation: SHA-256 salts incorporate the transport context to ensure unique key derivation per communication channel.
  • Directional isolation: HMAC-SHA256 derives independent client-to-exit and exit-to-client keys, preventing bidirectional cryptographic compromise.
  • AEAD enforcement: AES-256-GCM provides authenticated encryption with a 4096-nonce replay protection window.
  • Length validation: Mandatory 16-character minimum prevents weak shared secrets.

Frequently Asked Questions

Where does OpenFlux store its encryption keys?

OpenFlux does not store encryption keys internally. According to the source code in main/main.go, the binary expects an external file path via the --encryptionKeyFile flag. The operator maintains full custody of the key file, and OpenFlux only holds the derived cryptographic material in memory during runtime.

What key derivation function does OpenFlux use?

OpenFlux uses scrypt as implemented in transport/encrypted.go (lines 60–62). The function applies a memory-hard cost factor of 32768, block size of 8, and parallelization of 1 to derive a 32-byte master key. This KDF selection resists GPU and ASIC cracking attempts better than faster alternatives like PBKDF2.

Why does OpenFlux use separate keys for each direction?

The transport layer derives independent directional keys via HMAC-SHA256 keyed with the master secret and distinct domain strings ("client-to-exit" versus "exit-to-client"). This architectural choice prevents reflection attacks and ensures that compromising one traffic direction does not automatically decrypt the reverse flow, limiting blast radius in case of key exposure.

What happens if the encryption secret is shorter than 16 characters?

The NewEncryptedTransport constructor explicitly validates secret length and returns an error: "encryption secret must contain at least 16 characters". The program aborts transport initialization rather than proceeding with weak cryptographic material, enforcing a baseline security standard for all OpenFlux deployments.

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 →