Skip to main content

EncryptionClient

EncryptionClient is the client-side peer for EncryptionServer. It encrypts upstream payloads into TLS-like application-data records and decrypts downstream records back into plain WaterWall payloads.

Use it when you want AEAD protection inside a WaterWall stream chain without using a full TLS handshake.

warning

EncryptionClient is not TlsClient. It does not perform a TLS handshake, does not verify certificates, and does not provide SNI or ALPN. It only uses a TLS-like record shape for encrypted payload framing.

Typical Placement

Direct in-process pair:

TesterClient -> EncryptionClient -> EncryptionServer -> TesterServer

Two-server TCP tunnel:

client side: TcpListener -> EncryptionClient -> TcpConnector
server side: TcpListener -> EncryptionServer -> TcpConnector

The client and server settings must match. Any stream tunnels between them must preserve the encrypted record bytes end to end.

What It Does

  • Derives one 32-byte AEAD key from password, salt, and kdf-iterations.
  • Encrypts upstream payloads.
  • Splits upstream plaintext larger than max-frame-size into multiple records.
  • Wraps each encrypted chunk in a TLS-like application-data record.
  • Buffers downstream encrypted bytes until complete records are available.
  • Validates record headers and body lengths before decrypting.
  • Decrypts downstream records and forwards recovered plaintext to the previous node.
  • Closes both directions on malformed records, read-buffer overflow, or AEAD authentication failure.

EncryptionClient is a middle stream tunnel. It does not create sockets and it does not create packet lines.

Configuration Example

{
"name": "enc-client",
"type": "EncryptionClient",
"settings": {
"algorithm": "chacha20-poly1305",
"password": "replace-with-a-strong-secret",
"salt": "chain-a",
"kdf-iterations": 20000,
"max-frame-size": 16356
},
"next": "transport"
}

Matching server-side settings:

{
"name": "enc-server",
"type": "EncryptionServer",
"settings": {
"algorithm": "chacha20-poly1305",
"password": "replace-with-a-strong-secret",
"salt": "chain-a",
"kdf-iterations": 20000,
"max-frame-size": 16356
},
"next": "service"
}

Required Fields

Top-level fields:

FieldTypeDescription
namestringUser-chosen node name. Must be unique inside the config file.
typestringMust be exactly "EncryptionClient".
nextstringRequired. The next stream node receives encrypted records.
settingsobjectRequired. Must contain at least password.

Required settings fields:

FieldTypeDescription
passwordnon-empty stringShared secret used to derive the AEAD key. Must match the server side.

EncryptionClient must also have a previous node in the chain. It is not a chain head or chain end.

Optional Settings

SettingTypeDefaultDescription
algorithmstring or numeric enumchacha20-poly1305AEAD algorithm. method is accepted as a compatibility alias when algorithm is absent.
saltstringwaterwall-encryptionSalt input for the WaterWall key derivation process. Must match the server side.
kdf-iterationsinteger12000Key derivation rounds. Must be in range 1 through 1000000.
max-frame-sizeinteger16356Maximum plaintext bytes per encrypted record. Must be in range 1 through 16356.

Accepted algorithm strings:

AlgorithmAccepted values
ChaCha20-Poly1305chacha20-poly1305, chacha20poly1305, chacha20, chacha
AES-256-GCMaes-gcm, aes256gcm, aes-256-gcm, aes256-gcm

Numeric algorithm values are also accepted by the parser for compatibility with WaterWall's dynamic-value enum, but string values are clearer and recommended.

If AES-GCM is selected and the active crypto backend does not provide AES-GCM, startup fails.

Key Derivation

Both peers derive the same 32-byte key from:

  • password
  • salt
  • kdf-iterations

The implementation uses a WaterWall-specific BLAKE2s-based derivation loop, not PBKDF2 or Argon2. The derived key is stored in tunnel state and zeroed when the tunnel instance is destroyed.

For the encrypted path to work, both sides must use the same password, salt, kdf-iterations, and compatible algorithm.

Wire Format

Each encrypted payload chunk is encoded as:

5-byte TLS-like header | 12-byte nonce | AEAD ciphertext | 16-byte tag

Header fields:

FieldValue
record type0x17, TLS application data
version0x0303
body lengthBig-endian 16-bit length of nonce + ciphertext + tag

The 5-byte header is used as AEAD associated data. The selected algorithm is not sent in the record header; it must be configured consistently on both peers.

Limits and Padding

ValueSize
TLS-like header5 bytes
nonce12 bytes
AEAD tag16 bytes
maximum TLS-like record body16384 bytes
maximum plaintext per record16356 bytes
required left padding17 bytes

required_padding_left is 17, enough for the 5-byte header and 12-byte nonce that are prepended before encryption.

If an upstream plaintext payload is larger than max-frame-size, the client splits it into multiple encrypted records. Zero-length payload buffers are recycled and not forwarded.

Direction Behavior

DirectionBehavior
upstream payloadEncrypt, frame, and send to next.
downstream payloadBuffer, parse, decrypt, and send plaintext to prev.

Complete downstream frames may arrive split across multiple payload callbacks or several frames may arrive in one callback. The read stream keeps buffered bytes until complete records can be parsed.

If the buffered encrypted data grows beyond roughly two maximum records, the line is closed.

Lifecycle Behavior

On upstream Init, the client initializes its per-line read stream and forwards upstream Init to next.

On upstream Finish, it destroys local line state and forwards upstream Finish to next. On downstream Finish, it destroys local line state and forwards downstream Finish to prev.

Pause and resume are forwarded directionally:

CallbackForwarded to
upstream pause/resumenext tunnel upstream
downstream pause/resumeprevious tunnel downstream

Upstream Est and downstream Init are disabled callbacks for this node.

Notes and Caveats

  • Use it with a matching EncryptionServer.
  • This node protects payload bytes, but it does not hide that data is flowing inside WaterWall's TLS-like record format.
  • password should be long, random, and kept out of public configs.
  • max-frame-size should normally match on both sides. A receiver rejects records larger than its configured maximum.
  • Use TlsClient/TlsServer when you need real TLS semantics.