Encryption Pipeline

ENShell uses ECIES (Elliptic Curve Integrated Encryption Scheme) to ensure that AI agent instructions are encrypted end-to-end. The instruction plaintext exists only inside the CRE workflow during the brief window of analysis. Before that, it's ciphertext. After that, only the score and decision persist.

Why Encrypt?

The ActionSubmitted event on-chain contains only an instructionHash (a keccak256 hash of the instruction). The actual instruction content -- "Swap 0.05 ETH for USDC on Uniswap V2" -- never appears on the blockchain.

This matters because:

  • Instructions can contain sensitive intent -- trading strategies, business logic, proprietary workflows
  • On-chain data is public -- anyone watching the mempool or reading events can see plaintext
  • The CRE needs the plaintext -- Claude can't analyze a hash

ECIES solves this: the instruction is encrypted with a key that only the CRE oracle can decrypt, and the encrypted blob is stored off-chain on the relay.

The ECIES Construction

ENShell uses a standard ECIES construction built from three @noble primitives:

Primitive Library Purpose
secp256k1 ECDH @noble/secp256k1 Key agreement (shared secret derivation)
SHA-256 @noble/hashes Key derivation (shared secret → AES key)
AES-256-GCM @noble/ciphers Authenticated encryption

Key Pair

A secp256k1 key pair is generated for the oracle:

  • Private key -- 32 bytes, stored in Chainlink Vault DON (threshold-encrypted across oracle nodes in production, local .env in simulation)
  • Public key -- 33 bytes (compressed), shipped publicly in the SDK's NetworkConfig.oraclePublicKey

The public key for Sepolia: 02cea1f34f52c8e8a2d7d5bf4a768677e600be906fb5c68985fe635ac1331409ca

Encryption Flow (SDK side)

When protect() is called:

1. Generate ephemeral secp256k1 keypair
      ephemeralPrivate = random 32 bytes
      ephemeralPublic  = secp256k1.getPublicKey(ephemeralPrivate)  // 33 bytes compressed

2. ECDH key agreement
      sharedSecret = secp256k1.getSharedSecret(ephemeralPrivate, oraclePublicKey)

3. Key derivation
      aesKey = SHA-256(sharedSecret)  // 32 bytes

4. Encrypt
      nonce = random 12 bytes
      ciphertext = AES-256-GCM.encrypt(aesKey, nonce, utf8encode(instruction))

5. Pack
      output = ephemeralPublic (33) || nonce (12) || ciphertext (variable)
      return "0x" + hex(output)

The ephemeral key pair is generated fresh for every encryption. This provides forward secrecy per-message -- even if a previous ciphertext is somehow compromised, it reveals nothing about future messages.

Packed Wire Format

Offset    Length    Content
0         33        Compressed ephemeral public key (secp256k1)
33        12        AES-GCM nonce
45        variable  AES-GCM ciphertext (includes 16-byte auth tag)

The entire blob is hex-encoded with a 0x prefix and stored on the relay at PUT /relay/{instructionHash}.

Decryption Flow (CRE side)

Inside the CRE runtime, the workflow decrypts:

1. Unpack
      ephemeralPublic = bytes[0..33]
      nonce           = bytes[33..45]
      ciphertext      = bytes[45..]

2. ECDH key agreement
      sharedSecret = secp256k1.getSharedSecret(oraclePrivateKey, ephemeralPublic)

3. Key derivation
      aesKey = SHA-256(sharedSecret)

4. Decrypt
      plaintext = AES-256-GCM.decrypt(aesKey, nonce, ciphertext)
      return utf8decode(plaintext)

The decryption will fail (AES-GCM authentication error) if the wrong private key is used, providing cryptographic integrity verification.

Security Properties

Property Guarantee
Confidentiality Only the oracle private key holder can decrypt
Integrity AES-GCM authentication tag detects tampering
Forward secrecy Ephemeral keys per-message; compromising one doesn't compromise others
Non-determinism Same plaintext produces different ciphertext each time
Content addressing instructionHash = keccak256(plaintext) links on-chain hash to relay payload

Key Management

Oracle Key Pair

Environment Private Key Location Access Model
Simulation .env file on developer machine Single operator
Production Vault DON (Chainlink) Threshold-encrypted across oracle nodes

In the Vault DON model, the oracle private key is split across multiple oracle nodes using threshold cryptography. No single node (or operator) possesses the complete key. The key is only reassembled inside the CRE runtime during decryption.

Public Key Distribution

The oracle's compressed public key is baked into the SDK's NETWORK_CONFIG:

NETWORK_CONFIG[Network.SEPOLIA].oraclePublicKey
// "02cea1f34f52c8e8a2d7d5bf4a768677e600be906fb5c68985fe635ac1331409ca"

This is safe to distribute publicly -- knowing the public key doesn't help decrypt messages.

Implementation

SDK (encrypt)

import { encryptForOracle, NETWORK_CONFIG, Network } from '@enshell/sdk';

const oracleKey = NETWORK_CONFIG[Network.SEPOLIA].oraclePublicKey;
const encrypted = encryptForOracle('Swap 0.05 ETH for USDC', oracleKey);
// "0x02abc...def" (hex-encoded packed bytes)

CRE Workflow (decrypt)

The CRE workflow uses the same @noble libraries directly:

import { getSharedSecret } from '@noble/secp256k1';
import { gcm } from '@noble/ciphers/aes';
import { sha256 } from '@noble/hashes/sha256';

function decryptInstruction(encryptedHex: string, privateKeyHex: string): string {
  const bytes = hexToBytes(encryptedHex);
  const ephemeralPub = bytes.slice(0, 33);
  const nonce = bytes.slice(33, 45);
  const ciphertext = bytes.slice(45);

  const shared = getSharedSecret(privateKeyHex, ephemeralPub);
  const aesKey = sha256(shared);
  const plaintext = gcm(aesKey, nonce).decrypt(ciphertext);

  return new TextDecoder().decode(plaintext);
}

SDK (decrypt -- for testing/tooling)

import { decryptAsOracle } from '@enshell/sdk';

const plaintext = decryptAsOracle(encryptedHex, oraclePrivateKeyHex);

Zero External Dependencies

The encryption stack uses only @noble libraries:

  • @noble/secp256k1 -- Pure JavaScript secp256k1 implementation by Paul Miller. Audited, no dependencies.
  • @noble/ciphers -- AES-GCM implementation. Audited, no dependencies.
  • @noble/hashes -- SHA-256 implementation. Audited, no dependencies.

The @noble suite is widely used in the Ethereum ecosystem (ethers.js, viem) and has undergone multiple independent security audits.