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:
Encryption Flow
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)