How EncryptedTransport Derives Its AES-256-GCM Key in OpenFlux
EncryptedTransport derives its AES-256-GCM key using a three-stage process that combines scrypt-based master key generation, HMAC-SHA-256 directional splitting, and standard Go crypto/GCM construction to create independent bidirectional encryption streams.
The EncryptedTransport implementation in the OpenFlux repository transforms a shared secret into cryptographically independent encryption keys for secure tunneling. This process ensures that traffic flowing from client to exit node uses a completely different AES key than traffic returning from exit to client, providing strict directional secrecy.
Three-Stage Key Derivation Architecture
The derivation pipeline implemented in transport/encrypted.go converts a user-supplied secret and session context into two distinct 32-byte AES-256 keys. According to the OpenFlux source code, this involves sequential application of memory-hard key stretching, domain-separated HMAC derivation, and AEAD construction.
Stage 1: Master Key Generation with scrypt
The process begins by feeding the shared secret through scrypt to produce a 32-byte master key. The implementation uses SHA-256 to preprocess the salt, incorporating a versioned context string to prevent cross-protocol attacks.
salt := sha256.Sum256([]byte("OpenFlux encrypted transport v1\x00" + context))
master, err := scrypt.Key([]byte(secret), salt[:], 32768, 8, 1, 32)
Source: transport/encrypted.go lines 58-60
The scrypt parameters are explicitly set to N=32768, r=8, p=1, providing memory-hard protection against hardware-accelerated brute force attacks while yielding exactly 32 bytes (256 bits) required for AES-256.
Stage 2: Directional Sub-keys via HMAC-SHA-256
The master key undergoes domain separation to generate two independent directional keys. The deriveDirectionalKey helper function uses HMAC-SHA-256 with protocol-specific labels to ensure cryptographic independence between streams.
clientToExit := deriveDirectionalKey(master, "client-to-exit")
exitToClient := deriveDirectionalKey(master, "exit-to-client")
Source: transport/encrypted.go lines 63-65
The underlying HMAC implementation prefixes labels with a domain separator to prevent collision attacks:
func deriveDirectionalKey(master []byte, label string) []byte {
mac := hmac.New(sha256.New, master)
_, _ = mac.Write([]byte("OpenFlux direction v1\x00" + label))
return mac.Sum(nil)
}
Source: transport/encrypted.go lines 90-94
This construction ensures that knowledge of the client-to-exit key provides zero information about the exit-to-client key, even if the master key were somehow compromised.
Stage 3: AES-256-GCM Construction
Each 32-byte directional sub-key initializes a distinct AES-256-GCM AEAD instance. The newGCM factory function wraps Go's standard library cipher implementations to provide authenticated encryption with associated data (AEAD).
func newGCM(key []byte) (cipher.AEAD, error) {
block, err := aes.NewCipher(key)
aead, err := cipher.NewGCM(block)
return aead, nil
}
Source: transport/encrypted.go lines 96-104
The transport initializes one GCM instance for transmission and one for reception, selecting which directional key serves which purpose based on the exitNode boolean flag passed during construction.
Directional Key Selection Logic
When EncryptedTransport initializes, it evaluates the exitNode parameter to determine key assignment without requiring manual key configuration. If exitNode is false (client mode), the transport uses clientToExit for encryption (sending) and exitToClient for decryption (receiving). When exitNode is true, these assignments swap automatically.
This design eliminates the risk of nonce reuse across bidirectional communication channels, as each direction operates with an independent AES key and independent nonce space within the GCM construction.
Practical Implementation Example
The following example demonstrates initializing an encrypted transport on the client side using the exported constructor:
package main
import (
"log"
"github.com/p1neappleXpress/OpenFlux/main/transport"
)
// Example: create a client-side EncryptedTransport
func main() {
inner := transport.NewRawSocketTransport() // any implementation of Transport
secret := "super-secret-shared-key-1234"
context := "session-42" // public per-session identifier
exitNode := false // client, not exit
enc, err := transport.NewEncryptedTransport(inner, secret, context, exitNode)
if err != nil {
log.Fatalf("failed to initialise encrypted transport: %v", err)
}
// Use it like any other Transport
if err := enc.Send([]byte("Hello, exit node!")); err != nil {
log.Fatalf("send error: %v", err)
}
enc.Receive(func(p []byte) {
log.Printf("received: %s", string(p))
})
}
Running identical code on the exit node with exitNode := true automatically swaps the encryption and decryption keys, preserving directional secrecy without additional configuration.
Summary
- Scrypt-based stretching in
transport/encrypted.go(lines 58-60) generates a 32-byte master key using SHA-256-derived salts and memory-hard parameters (32768, 8, 1). - HMAC-SHA-256 domain separation creates two independent directional keys via
deriveDirectionalKey(lines 90-94), preventing cross-direction key leakage. - AES-256-GCM construction (lines 96-104) wraps each directional key in authenticated encryption mode, providing confidentiality and integrity.
- Automatic key selection based on the
exitNodeboolean ensures clients and exits use complementary key pairs without manual coordination.
Frequently Asked Questions
What hashing algorithm does EncryptedTransport use for the salt?
EncryptedTransport uses SHA-256 to hash the concatenation of a versioned protocol string ("OpenFlux encrypted transport v1") and the user-provided context string before passing it as the salt to scrypt. This ensures deterministic salt generation while binding the key to the specific protocol version.
Why does OpenFlux use scrypt instead of PBKDF2?
OpenFlux selects scrypt specifically for its memory-hard properties. Unlike PBKDF2, which is vulnerable to GPU and ASIC acceleration due to its low memory requirements, scrypt with parameters N=32768, r=8 requires significant memory to compute, making brute-force attacks against the shared secret substantially more expensive.
How does the transport prevent key reuse between directions?
The transport prevents key reuse by deriving two cryptographically independent keys from the master key using HMAC-SHA-256 with distinct, domain-separated labels ("client-to-exit" and "exit-to-client"). Because HMAC acts as a pseudorandom function, the two directional keys are computationally indistinguishable from random and independent of each other, ensuring that nonce spaces remain isolated between send and receive operations.
Where is the deriveDirectionalKey function defined?
The deriveDirectionalKey helper function is defined in transport/encrypted.go at lines 90-94. It accepts a master key byte slice and a direction label string, then returns a 32-byte derived key suitable for AES-256-GCM initialization via HMAC-SHA-256 processing.
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 →