End-to-End Encryption

Opacus Protocol uses modern cryptographic primitives to ensure all agent-to-agent communication is encrypted, authenticated, and tamper-proof.

Encryption Overview

Opacus implements a multi-layered cryptographic approach:

🔒 X25519 ECDH: Elliptic curve Diffie-Hellman for shared secret derivation
🔑 HKDF: HMAC-based Key Derivation Function for encryption key generation
🛡️ ChaCha20-Poly1305: Authenticated encryption with associated data (AEAD)
HMAC-SHA256: Message authentication codes for integrity verification

Encryption Flow

Agent-to-Agent Encryption Flow Alice (Sender) Private Key: a_priv Public Key: a_pub Plaintext Message: "Secret data for Bob" 1. Derive Shared Secret (ECDH) shared = X25519(a_priv, b_pub) Both Alice and Bob compute same shared secret: shared = 0x3f7a2b... (32 bytes) Bob (Receiver) Private Key: b_priv Public Key: b_pub ← Fetch from chain 2. Derive Encryption Key (HKDF) enc_key = HKDF-SHA256(shared, salt, info, 32) enc_key = 0x8c1d... (32 bytes) 3. Encrypt Message nonce = random(12 bytes) ciphertext = ChaCha20-Poly1305 (plaintext, enc_key, nonce) 4. Generate MAC mac = HMAC-SHA256 (ciphertext + nonce, enc_key) Ensures integrity 5. Send Encrypted package = { ciphertext, nonce, mac } 6. Bob Decrypts 1. Verify MAC (ensures no tampering) 2. Decrypt: plaintext = ChaCha20-Poly1305 (ciphertext, enc_key, nonce) ✅ "Secret data for Bob"

Cryptographic Primitives

1. X25519 Key Exchange

X25519 is used for deriving a shared secret between two agents without transmitting the secret over the network:

// TypeScript
import { x25519 } from '@noble/curves/ed25519';

// Alice's keypair
const alicePrivate = x25519.utils.randomPrivateKey();
const alicePublic = x25519.getPublicKey(alicePrivate);

// Bob's public key (fetched from blockchain)
const bobPublic = await client.getAgent(bobId).then(a => a.xPublicKey);

// Derive shared secret
const sharedSecret = x25519.getSharedSecret(alicePrivate, bobPublic);
console.log('Shared secret:', Buffer.from(sharedSecret).toString('hex'));
// Rust
use x25519_dalek::{StaticSecret, PublicKey};

// Alice's keypair
let alice_secret = StaticSecret::random_from_rng(OsRng);
let alice_public = PublicKey::from(&alice_secret);

// Bob's public key (fetched from blockchain)
let bob_public = PublicKey::from(bob_public_bytes);

// Derive shared secret
let shared_secret = alice_secret.diffie_hellman(&bob_public);
println!("Shared secret: {:?}", shared_secret.as_bytes());

2. HKDF Key Derivation

HKDF transforms the shared secret into a strong encryption key:

// TypeScript
import { hkdf } from '@noble/hashes/hkdf';
import { sha256 } from '@noble/hashes/sha256';

const salt = new Uint8Array(32); // Optional salt
const info = new TextEncoder().encode('opacus-encryption-v1');

const encryptionKey = hkdf(
  sha256,
  sharedSecret,
  salt,
  info,
  32 // 256-bit key
);

console.log('Encryption key:', Buffer.from(encryptionKey).toString('hex'));
// Rust
use hkdf::Hkdf;
use sha2::Sha256;

let salt = [0u8; 32];
let info = b"opacus-encryption-v1";

let hk = Hkdf::::new(Some(&salt), shared_secret.as_bytes());
let mut encryption_key = [0u8; 32];
hk.expand(info, &mut encryption_key).unwrap();

println!("Encryption key: {:?}", encryption_key);

3. ChaCha20-Poly1305 AEAD

Authenticated encryption provides both confidentiality and integrity:

// TypeScript
import { chacha20poly1305 } from '@noble/ciphers/chacha';
import { randomBytes } from '@noble/hashes/utils';

// Generate random nonce
const nonce = randomBytes(12); // 96 bits

// Encrypt
const cipher = chacha20poly1305(encryptionKey, nonce);
const plaintext = new TextEncoder().encode('Secret message');
const ciphertext = cipher.encrypt(plaintext);

console.log('Encrypted:', Buffer.from(ciphertext).toString('hex'));

// Decrypt
const decrypted = cipher.decrypt(ciphertext);
console.log('Decrypted:', new TextDecoder().decode(decrypted));
// Rust
use chacha20poly1305::{ChaCha20Poly1305, Key, Nonce};
use chacha20poly1305::aead::{Aead, NewAead};
use rand::RngCore;

// Generate random nonce
let mut nonce_bytes = [0u8; 12];
OsRng.fill_bytes(&mut nonce_bytes);
let nonce = Nonce::from_slice(&nonce_bytes);

// Create cipher
let key = Key::from_slice(&encryption_key);
let cipher = ChaCha20Poly1305::new(key);

// Encrypt
let plaintext = b"Secret message";
let ciphertext = cipher.encrypt(nonce, plaintext.as_ref()).unwrap();

// Decrypt
let decrypted = cipher.decrypt(nonce, ciphertext.as_ref()).unwrap();
println!("Decrypted: {:?}", String::from_utf8(decrypted));

4. HMAC-SHA256 Authentication

HMAC provides message authentication and integrity:

// TypeScript
import { hmac } from '@noble/hashes/hmac';
import { sha256 } from '@noble/hashes/sha256';

const message = Buffer.concat([ciphertext, nonce]);
const mac = hmac(sha256, encryptionKey, message);

console.log('MAC:', Buffer.from(mac).toString('hex'));

// Verify
const isValid = hmac.verify(mac, encryptionKey, message);
console.log('Valid:', isValid);
// Rust
use hmac::{Hmac, Mac};
use sha2::Sha256;

type HmacSha256 = Hmac;

let mut message = ciphertext.clone();
message.extend_from_slice(&nonce_bytes);

// Generate MAC
let mut mac = HmacSha256::new_from_slice(&encryption_key).unwrap();
mac.update(&message);
let result = mac.finalize();
let mac_bytes = result.into_bytes();

println!("MAC: {:?}", mac_bytes);

// Verify
let mut mac_verify = HmacSha256::new_from_slice(&encryption_key).unwrap();
mac_verify.update(&message);
mac_verify.verify(&mac_bytes).unwrap();

Security Properties

🔒

Confidentiality

Only sender and receiver can read messages
ChaCha20 stream cipher
128-bit security level

Integrity

Tampering is detectable
Poly1305 MAC tag
Cryptographic guarantee

🛡️

Authentication

Sender identity verified
Ed25519 signatures
Non-repudiation

🔑

Forward Secrecy

Past messages stay secure
Ephemeral key exchange
Key rotation support

Complete Example

TypeScript Full Encryption

import { OpacusClient } from '@opacus/sdk';
import { x25519 } from '@noble/curves/ed25519';
import { hkdf } from '@noble/hashes/hkdf';
import { sha256 } from '@noble/hashes/sha256';
import { chacha20poly1305 } from '@noble/ciphers/chacha';
import { randomBytes } from '@noble/hashes/utils';

async function encryptForAgent(
  recipientAgentId: string,
  plaintext: Uint8Array
): Promise {
  // 1. Get recipient's public key from blockchain
  const recipient = await client.getAgent(recipientAgentId);
  const recipientPublicKey = recipient.xPublicKey;
  
  // 2. Generate ephemeral keypair
  const ephemeralPrivate = x25519.utils.randomPrivateKey();
  const ephemeralPublic = x25519.getPublicKey(ephemeralPrivate);
  
  // 3. Derive shared secret
  const sharedSecret = x25519.getSharedSecret(
    ephemeralPrivate,
    recipientPublicKey
  );
  
  // 4. Derive encryption key
  const encKey = hkdf(sha256, sharedSecret, undefined, undefined, 32);
  
  // 5. Encrypt message
  const nonce = randomBytes(12);
  const cipher = chacha20poly1305(encKey, nonce);
  const ciphertext = cipher.encrypt(plaintext);
  
  return {
    ephemeralPublic,
    nonce,
    ciphertext
  };
}

Rust Full Encryption

use x25519_dalek::{StaticSecret, PublicKey};
use hkdf::Hkdf;
use sha2::Sha256;
use chacha20poly1305::{ChaCha20Poly1305, Key, Nonce};
use chacha20poly1305::aead::{Aead, NewAead};
use rand::rngs::OsRng;

pub struct EncryptedMessage {
    pub ephemeral_public: [u8; 32],
    pub nonce: [u8; 12],
    pub ciphertext: Vec,
}

pub async fn encrypt_for_agent(
    recipient_id: &str,
    plaintext: &[u8]
) -> Result {
    // 1. Get recipient's public key from blockchain
    let recipient = client.get_agent(recipient_id).await?;
    let recipient_public = PublicKey::from(recipient.x_public_key);
    
    // 2. Generate ephemeral keypair
    let ephemeral_secret = StaticSecret::random_from_rng(OsRng);
    let ephemeral_public = PublicKey::from(&ephemeral_secret);
    
    // 3. Derive shared secret
    let shared_secret = ephemeral_secret.diffie_hellman(&recipient_public);
    
    // 4. Derive encryption key
    let hk = Hkdf::::new(None, shared_secret.as_bytes());
    let mut enc_key = [0u8; 32];
    hk.expand(&[], &mut enc_key)?;
    
    // 5. Encrypt message
    let mut nonce_bytes = [0u8; 12];
    OsRng.fill_bytes(&mut nonce_bytes);
    
    let key = Key::from_slice(&enc_key);
    let cipher = ChaCha20Poly1305::new(key);
    let nonce = Nonce::from_slice(&nonce_bytes);
    
    let ciphertext = cipher.encrypt(nonce, plaintext)?;
    
    Ok(EncryptedMessage {
        ephemeral_public: ephemeral_public.to_bytes(),
        nonce: nonce_bytes,
        ciphertext,
    })
}

🔐 Best Practices

  • ✅ Always use fresh random nonces (never reuse!)
  • ✅ Verify MAC before decrypting to prevent tampering
  • ✅ Use ephemeral keys for forward secrecy
  • ✅ Rotate keys periodically for long-lived agents
  • ✅ Store private keys securely (hardware wallets, encrypted storage)