Skip to main content
Privora supports dual encryption where values are encrypted both with TFHE (for homomorphic computation) and with the user’s X25519 key (for local recovery). This allows users to view their own data without requiring MPC decryption.

Why Dual Encryption?

Standard FHE decryption requires the network’s private key, which is distributed across MPC nodes. This means:
  • Decryption requires a threshold of MPC nodes to participate
  • Users must wait for the decryption protocol to complete
  • Each decryption request has network overhead
Dual encryption solves this by adding a second encryption layer that users can decrypt locally.

How It Works

When submitting encrypted data with dual encryption:
  1. TFHE encryption: Value is encrypted with the network’s FHE public key
  2. X25519 encryption: Same value is encrypted with user’s X25519 public key
  3. Submission: Both ciphertexts are submitted together
  4. Recovery: User can decrypt the X25519 ciphertext locally anytime

Client Implementation

Rust Client

use privora_sdk_client::prelude::*;

let privora = PrivoraClient::new("http://localhost:8899").await?;
let keypair = Keypair::new();

// Create user crypto from Solana keypair
let user_crypto = privora.user_crypto(&keypair)?;

// Encrypt with FHE
let encrypted = privora.encryptor().encrypt(100u8)?;

// Add user recovery data
let plaintext_bytes = 100u8.to_le_bytes();
let with_recovery = user_crypto.add_recovery(encrypted, &plaintext_bytes)?;

// Submit (includes both FHE ciphertext and recovery data)
let hash = privora.submit(&with_recovery).await?;

// Later: Recover locally without MPC
let recovery_data = with_recovery.user_recovery().unwrap();
let recovered = user_crypto.decrypt_recovery(recovery_data)?;
let value = u8::from_le_bytes(recovered.try_into().unwrap());
assert_eq!(value, 100);

TypeScript Client

const privora = await Privora.connect('http://localhost:8899');
const keypair = Keypair.generate();

// Create user crypto
const userCrypto = privora.userCrypto(keypair);

// Encrypt with user recovery
const encrypted = privora
  .encrypt(100, 'u8')
  .withUserRecovery(userCrypto);

// Submit
const hash = await privora.submit(encrypted);

// Later: Recover locally
const plaintext = userCrypto.decryptRecovery(encrypted.userRecovery);

X25519 Key Derivation

Privora derives X25519 keys from Solana ed25519 keypairs:
use privora_sdk_client::encryption::UserCrypto;

// From Solana keypair
let solana_keypair = Keypair::new();
let user_crypto = UserCrypto::from_keypair(&solana_keypair)?;

// Get X25519 public key (for sharing)
let x25519_pubkey = user_crypto.x25519_public_key();
let pubkey_bytes: [u8; 32] = user_crypto.public_key_bytes();
This conversion is deterministic:
  • Same Solana keypair always produces same X25519 keypair
  • Compatible with standard ed25519-to-X25519 conversion

Encryption Details

Encryption Scheme

User recovery uses XSalsa20-Poly1305 authenticated encryption:
ComponentDescription
AlgorithmXSalsa20-Poly1305 (crypto_box)
Key ExchangeX25519 ECDH
Nonce24 bytes, randomly generated
AuthenticationPoly1305 MAC

Data Format

UserRecoveryData {
    ciphertext: Vec<u8>,  // Encrypted plaintext
    nonce: [u8; 24],      // Random nonce
}

Use Cases

1. Portfolio Display

Users can view their encrypted balances locally:
// User wants to see their balance
const balance = userCrypto.decryptRecovery(account.balanceRecovery);
displayBalance(balance);

// No MPC request needed!

2. Order History

Users can review their order details:
// Decrypt order details for display
const price = userCrypto.decryptRecovery(order.priceRecovery);
const quantity = userCrypto.decryptRecovery(order.quantityRecovery);
showOrderDetails(price, quantity);

3. Receipt Generation

Generate receipts without network requests:
// User exports transaction receipt
const amount = userCrypto.decryptRecovery(tx.amountRecovery);
const receipt = generateReceipt(tx.id, amount, tx.timestamp);

When to Use Dual Encryption

Use Dual Encryption

  • User’s own data they’ll want to view
  • Balances, orders, positions
  • Data that doesn’t need to be hidden from the user

Skip Dual Encryption

  • Third-party data (e.g., counterparty’s order)
  • Computed results that should remain private
  • System-generated values

Security Considerations

What Users Can See

With dual encryption, users can decrypt:
  • Values they encrypted themselves
  • Only their own recovery data

What Users Cannot See

Users cannot decrypt:
  • Other users’ encrypted values
  • FHE computation results (unless authorized)
  • System-encrypted values

Key Security

  • X25519 private key is derived from Solana private key
  • Same security model as Solana keypair management
  • Recovery data should be stored securely (e.g., encrypted storage)

Comparison: MPC vs User Recovery

AspectMPC DecryptionUser Recovery
LatencyNetwork round-tripInstant (local)
AuthorizationRequires auth PDAAlways available to owner
TrustThreshold trust (k-of-n)User’s own key
Use CaseCross-party revealsViewing own data

Implementation Notes

Storage Trade-offs

Dual encryption increases storage:
DataSize
FHE ciphertext only~10-80KB
User recovery data~50 bytes
Total increaseNegligible

Performance

User recovery is fast:
  • X25519 ECDH: ~microseconds
  • XSalsa20-Poly1305: ~microseconds
  • No network requests

Next Steps