12 KiB
RFC-0001: Pre-Obfuscation Layer Protocol for AEAD Ciphers
Status: Informational Version: 1.0 Created: 2025-01-13 Author: Snider
Abstract
This document specifies a pre-obfuscation layer protocol designed to transform plaintext data before it reaches CPU encryption routines. The protocol provides an additional security layer that prevents raw plaintext patterns from being processed directly by encryption hardware, mitigating potential side-channel attack vectors while maintaining full compatibility with standard AEAD cipher constructions.
Table of Contents
- Introduction
- Terminology
- Protocol Overview
- Obfuscator Implementations
- Integration with AEAD Ciphers
- Wire Format
- Security Considerations
- Implementation Requirements
- Test Vectors
- References
1. Introduction
Modern AEAD (Authenticated Encryption with Associated Data) ciphers like ChaCha20-Poly1305 and AES-GCM provide strong cryptographic guarantees. However, the plaintext data is processed directly by CPU encryption instructions, potentially exposing patterns through side-channel attacks such as timing analysis, power analysis, or electromagnetic emanation.
This RFC defines a pre-obfuscation layer that transforms plaintext into an unpredictable byte sequence before encryption. The transformation is reversible, deterministic (given the same entropy source), and adds negligible overhead while providing defense-in-depth against side-channel attacks.
1.1 Design Goals
- Reversibility: All transformations MUST be perfectly reversible
- Determinism: Given the same entropy, transformations MUST produce identical results
- Independence: The obfuscation layer operates independently of the underlying cipher
- Zero overhead on security: The underlying AEAD cipher's security properties are preserved
- Minimal computational overhead: Transformations should add < 5% processing time
2. Terminology
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119.
Plaintext: The original data to be encrypted Obfuscated data: Plaintext after pre-obfuscation transformation Ciphertext: Obfuscated data after encryption Entropy: A source of randomness used to derive transformation parameters (typically the nonce) Key stream: A deterministic sequence of bytes derived from entropy Permutation: A bijective mapping of byte positions
3. Protocol Overview
The pre-obfuscation protocol operates in two stages:
3.1 Encryption Flow
Plaintext --> Obfuscate(plaintext, entropy) --> Obfuscated --> Encrypt --> Ciphertext
- Generate cryptographic nonce for the AEAD cipher
- Apply obfuscation transformation using nonce as entropy
- Encrypt the obfuscated data using the AEAD cipher
- Output:
[nonce || ciphertext || auth_tag]
3.2 Decryption Flow
Ciphertext --> Decrypt --> Obfuscated --> Deobfuscate(obfuscated, entropy) --> Plaintext
- Extract nonce from the ciphertext prefix
- Decrypt the ciphertext using the AEAD cipher
- Apply reverse obfuscation transformation using the extracted nonce
- Output: Original plaintext
3.3 Entropy Derivation
The entropy source MUST be the same value used as the AEAD cipher nonce. This ensures:
- No additional random values need to be generated or stored
- The obfuscation is tied to the specific encryption operation
- Replay of ciphertext with different obfuscation is not possible
4. Obfuscator Implementations
This RFC defines two standard obfuscator implementations. Implementations MAY support additional obfuscators provided they meet the requirements in Section 8.
4.1 XOR Obfuscator
The XOR obfuscator generates a deterministic key stream from the entropy and XORs it with the plaintext.
4.1.1 Key Stream Derivation
function deriveKeyStream(entropy: bytes, length: int) -> bytes:
stream = empty byte array of size length
blockNum = 0
offset = 0
while offset < length:
block = SHA256(entropy || BigEndian64(blockNum))
copyLen = min(32, length - offset)
copy block[0:copyLen] to stream[offset:offset+copyLen]
offset += copyLen
blockNum += 1
return stream
4.1.2 Obfuscation
function obfuscate(data: bytes, entropy: bytes) -> bytes:
if length(data) == 0:
return data
keyStream = deriveKeyStream(entropy, length(data))
result = new byte array of size length(data)
for i = 0 to length(data) - 1:
result[i] = data[i] XOR keyStream[i]
return result
4.1.3 Deobfuscation
The XOR operation is symmetric; deobfuscation uses the same algorithm:
function deobfuscate(data: bytes, entropy: bytes) -> bytes:
return obfuscate(data, entropy) // XOR is self-inverse
4.2 Shuffle-Mask Obfuscator
The shuffle-mask obfuscator provides additional diffusion by combining a byte-level shuffle with an XOR mask.
4.2.1 Permutation Generation
Uses Fisher-Yates shuffle with deterministic randomness:
function generatePermutation(entropy: bytes, length: int) -> int[]:
perm = [0, 1, 2, ..., length-1]
seed = SHA256(entropy || "permutation")
for i = length-1 downto 1:
hash = SHA256(seed || BigEndian64(i))
j = BigEndian64(hash[0:8]) mod (i + 1)
swap perm[i] and perm[j]
return perm
4.2.2 Mask Derivation
function deriveMask(entropy: bytes, length: int) -> bytes:
mask = empty byte array of size length
blockNum = 0
offset = 0
while offset < length:
block = SHA256(entropy || "mask" || BigEndian64(blockNum))
copyLen = min(32, length - offset)
copy block[0:copyLen] to mask[offset:offset+copyLen]
offset += copyLen
blockNum += 1
return mask
4.2.3 Obfuscation
function obfuscate(data: bytes, entropy: bytes) -> bytes:
if length(data) == 0:
return data
perm = generatePermutation(entropy, length(data))
mask = deriveMask(entropy, length(data))
// Step 1: Apply mask
masked = new byte array of size length(data)
for i = 0 to length(data) - 1:
masked[i] = data[i] XOR mask[i]
// Step 2: Shuffle bytes according to permutation
shuffled = new byte array of size length(data)
for i = 0 to length(data) - 1:
shuffled[i] = masked[perm[i]]
return shuffled
4.2.4 Deobfuscation
function deobfuscate(data: bytes, entropy: bytes) -> bytes:
if length(data) == 0:
return data
perm = generatePermutation(entropy, length(data))
mask = deriveMask(entropy, length(data))
// Step 1: Unshuffle bytes (inverse permutation)
unshuffled = new byte array of size length(data)
for i = 0 to length(data) - 1:
unshuffled[perm[i]] = data[i]
// Step 2: Remove mask
result = new byte array of size length(data)
for i = 0 to length(data) - 1:
result[i] = unshuffled[i] XOR mask[i]
return result
5. Integration with AEAD Ciphers
5.1 XChaCha20-Poly1305 Integration
When used with XChaCha20-Poly1305:
- Nonce size: 24 bytes
- Key size: 32 bytes
- Auth tag size: 16 bytes
function encrypt(key: bytes[32], plaintext: bytes) -> bytes:
nonce = random_bytes(24)
obfuscated = obfuscator.obfuscate(plaintext, nonce)
ciphertext = XChaCha20Poly1305_Seal(key, nonce, obfuscated, nil)
return nonce || ciphertext // nonce is prepended
function decrypt(key: bytes[32], data: bytes) -> bytes:
if length(data) < 24 + 16: // nonce + auth tag minimum
return error("ciphertext too short")
nonce = data[0:24]
ciphertext = data[24:]
obfuscated = XChaCha20Poly1305_Open(key, nonce, ciphertext, nil)
plaintext = obfuscator.deobfuscate(obfuscated, nonce)
return plaintext
5.2 Other AEAD Ciphers
The pre-obfuscation layer is cipher-agnostic. For other AEAD ciphers:
| Cipher | Nonce Size | Notes |
|---|---|---|
| AES-128-GCM | 12 bytes | Standard nonce |
| AES-256-GCM | 12 bytes | Standard nonce |
| ChaCha20-Poly1305 | 12 bytes | Original ChaCha nonce |
| XChaCha20-Poly1305 | 24 bytes | Extended nonce (RECOMMENDED) |
6. Wire Format
The output wire format is:
+----------------+------------------------+
| Nonce | Ciphertext |
+----------------+------------------------+
| N bytes | len(plaintext) + T |
Where:
N= Nonce size (cipher-dependent)T= Authentication tag size (typically 16 bytes)
The obfuscation parameters are NOT stored in the wire format. They are derived deterministically from the nonce.
7. Security Considerations
7.1 Side-Channel Mitigation
The pre-obfuscation layer provides defense-in-depth against:
- Timing attacks: Plaintext patterns do not influence encryption timing
- Cache-timing attacks: Memory access patterns are decorrelated from plaintext
- Power analysis: Power consumption patterns are decorrelated from plaintext structure
7.2 Cryptographic Security
The pre-obfuscation layer does NOT provide cryptographic security on its own. It MUST always be used in conjunction with a proper AEAD cipher. The security of the combined system relies entirely on the underlying AEAD cipher's security guarantees.
7.3 Entropy Requirements
The entropy source (nonce) MUST be generated using a cryptographically secure random number generator. Nonce reuse with the same key compromises both the obfuscation determinism and the AEAD security.
7.4 Key Stream Exhaustion
The XOR obfuscator uses SHA-256 in counter mode. For a single encryption:
- Maximum safely obfuscated data: 2^64 * 32 bytes (theoretical)
- Practical limit: Constrained by AEAD cipher limits
7.5 Permutation Uniqueness
The shuffle-mask obfuscator generates permutations deterministically. For data of length n:
- Total possible permutations: n!
- Entropy required for full permutation space: log2(n!) bits
- SHA-256 provides 256 bits, sufficient for n up to ~57 bytes without collision concerns
For larger data, the permutation space is sampled uniformly but not exhaustively.
8. Implementation Requirements
Conforming implementations MUST:
- Support at least the XOR obfuscator
- Use SHA-256 for key stream and permutation derivation
- Use big-endian byte ordering for block numbers
- Handle zero-length data by returning it unchanged
- Prepend the nonce to the ciphertext output
- Accept and process the nonce from ciphertext prefix during decryption
Conforming implementations SHOULD:
- Support the shuffle-mask obfuscator
- Use XChaCha20-Poly1305 as the default AEAD cipher
- Provide constant-time implementations where feasible
9. Test Vectors
9.1 XOR Obfuscator
Entropy (hex): 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f
Plaintext (hex): 48656c6c6f2c20576f726c6421
Expected key stream prefix (hex): [first 14 bytes of SHA256(entropy || 0x0000000000000000)]
9.2 Shuffle-Mask Obfuscator
Entropy (hex): 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f
Plaintext: "Hello"
Permutation seed: SHA256(entropy || "permutation")
Mask seed: SHA256(entropy || "mask" || 0x0000000000000000)
10. Future Work
- Hardware-accelerated obfuscation implementations
- Additional obfuscator algorithms (block-based, etc.)
- Formal side-channel resistance analysis
- Integration benchmarks with different AEAD ciphers
- WASM compilation for browser environments
11. References
- [RFC 8439] ChaCha20 and Poly1305 for IETF Protocols
- [RFC 7539] ChaCha20 and Poly1305 for IETF Protocols (obsoleted by 8439)
- [draft-irtf-cfrg-xchacha] XChaCha: eXtended-nonce ChaCha and AEAD_XChaCha20_Poly1305
- [FIPS 180-4] Secure Hash Standard (SHA-256)
- Fisher, R. A.; Yates, F. (1948). Statistical tables for biological, agricultural and medical research
Appendix A: Reference Implementation
A reference implementation in Go is available at:
github.com/Snider/Enchantrix/pkg/enchantrix/crypto_sigil.go
Appendix B: Changelog
- 1.0 (2025-01-13): Initial specification